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.
Files changed (94) hide show
  1. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/PKG-INFO +2 -2
  2. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/README.md +1 -1
  3. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/administration/api-keys.mdx +2 -2
  4. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/administration/backups.mdx +1 -1
  5. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/deployment/cost-management.mdx +1 -1
  6. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/introduction.mdx +3 -3
  7. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/sdk/quickstart.mdx +3 -3
  8. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/basic_agent.py +1 -0
  9. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/deploy.sh +4 -0
  10. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/production.sh +4 -0
  11. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/setup.sh +4 -0
  12. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/pyproject.toml +2 -1
  13. nightshift_sdk-0.4.3/src/nightshift/cli/commands/agents.py +101 -0
  14. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/deploy.py +10 -0
  15. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/events.py +10 -3
  16. nightshift_sdk-0.4.3/src/nightshift/protocol/events.py +56 -0
  17. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/registry.py +26 -0
  18. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/sdk/config.py +1 -0
  19. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/server.py +164 -20
  20. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/task.py +12 -2
  21. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/vm/manager.py +53 -4
  22. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_events.py +49 -2
  23. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_registry.py +47 -0
  24. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/uv.lock +2 -2
  25. nightshift_sdk-0.4.2/src/nightshift/cli/commands/agents.py +0 -56
  26. nightshift_sdk-0.4.2/src/nightshift/protocol/events.py +0 -36
  27. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/.claude/skills/opentui +0 -0
  28. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/.github/workflows/release.yml +0 -0
  29. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/.gitignore +0 -0
  30. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/CLAUDE.md +0 -0
  31. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/LICENSE +0 -0
  32. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/contributing/deploying-changes.mdx +0 -0
  33. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/contributing/dev-setup.mdx +0 -0
  34. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/deployment/architecture.mdx +0 -0
  35. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/deployment/provision.mdx +0 -0
  36. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/deployment/server.mdx +0 -0
  37. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/deployment/verify.mdx +0 -0
  38. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/docs.json +0 -0
  39. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/logo/kokapo.gif +0 -0
  40. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/logo/kokapo.svg +0 -0
  41. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/logo/nightshift-text.png +0 -0
  42. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/docs/sdk/agent-config.mdx +0 -0
  43. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/code_reviewer.py +0 -0
  44. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/edit_agent.py +0 -0
  45. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/hooks_agent.py +0 -0
  46. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/mcp_agent.py +0 -0
  47. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/multi_agent.py +0 -0
  48. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/net_test.py +0 -0
  49. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/oai_agent.py +0 -0
  50. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/pyproject.toml +0 -0
  51. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/smoke_test.py +0 -0
  52. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/stateful_agent.py +0 -0
  53. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/test-workspace/hello.txt +0 -0
  54. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/examples/web_researcher.py +0 -0
  55. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/bake-rootfs.sh +0 -0
  56. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/.gitignore +0 -0
  57. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/cron.sh +0 -0
  58. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/deploy.sh +0 -0
  59. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/lambda/start.py +0 -0
  60. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/lambda/stop.py +0 -0
  61. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/teardown.sh +0 -0
  62. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/dev/test.sh +0 -0
  63. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/infra/upgrade.sh +0 -0
  64. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/rootfs/build-rootfs.sh +0 -0
  65. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/rootfs/init +0 -0
  66. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/__init__.py +0 -0
  67. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/__main__.py +0 -0
  68. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/agent/__init__.py +0 -0
  69. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/agent/__main__.py +0 -0
  70. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/agent/entry.py +0 -0
  71. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/auth.py +0 -0
  72. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/__init__.py +0 -0
  73. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/__init__.py +0 -0
  74. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/api_key.py +0 -0
  75. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/login.py +0 -0
  76. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/logs.py +0 -0
  77. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/commands/run.py +0 -0
  78. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/config.py +0 -0
  79. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/cli/main.py +0 -0
  80. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/config.py +0 -0
  81. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/protocol/__init__.py +0 -0
  82. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/protocol/packaging.py +0 -0
  83. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/sdk/__init__.py +0 -0
  84. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/sdk/app.py +0 -0
  85. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/vm/__init__.py +0 -0
  86. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/vm/network.py +0 -0
  87. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/vm/pool.py +0 -0
  88. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/src/nightshift/vm/rootfs.py +0 -0
  89. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/__init__.py +0 -0
  90. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_config.py +0 -0
  91. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_deploy_api.py +0 -0
  92. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_deploy_cli.py +0 -0
  93. {nightshift_sdk-0.4.2 → nightshift_sdk-0.4.3}/tests/test_protocol.py +0 -0
  94. {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.2
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
@@ -20,6 +20,7 @@ app = NightshiftApp()
20
20
  vcpu_count=2,
21
21
  mem_size_mib=2048,
22
22
  # max_concurrent_vms=2,
23
+ forward_secrets=["ANTHROPIC_API_KEY"],
23
24
  stateful=True,
24
25
  )
25
26
  )
@@ -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.2"
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