nightshift-sdk 0.4.0__tar.gz → 0.4.2__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/.github/workflows/release.yml +79 -0
  2. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/.gitignore +1 -0
  3. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/PKG-INFO +1 -1
  4. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/deployment/provision.mdx +27 -12
  5. nightshift_sdk-0.4.2/examples/oai_agent.py +22 -0
  6. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/infra/bake-rootfs.sh +3 -43
  7. nightshift_sdk-0.4.2/infra/deploy.sh +362 -0
  8. {nightshift_sdk-0.4.0/infra → nightshift_sdk-0.4.2/infra/dev}/cron.sh +2 -2
  9. {nightshift_sdk-0.4.0/infra → nightshift_sdk-0.4.2/infra/dev}/deploy.sh +4 -4
  10. {nightshift_sdk-0.4.0/infra → nightshift_sdk-0.4.2/infra/dev}/production.sh +5 -5
  11. {nightshift_sdk-0.4.0/infra → nightshift_sdk-0.4.2/infra/dev}/setup.sh +7 -7
  12. {nightshift_sdk-0.4.0/infra → nightshift_sdk-0.4.2/infra/dev}/teardown.sh +2 -2
  13. {nightshift_sdk-0.4.0/infra → nightshift_sdk-0.4.2/infra/dev}/test.sh +6 -6
  14. nightshift_sdk-0.4.2/infra/upgrade.sh +112 -0
  15. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/pyproject.toml +1 -1
  16. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/rootfs/build-rootfs.sh +27 -10
  17. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/rootfs/init +3 -2
  18. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/agent/entry.py +28 -12
  19. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/cli/commands/run.py +7 -8
  20. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/config.py +1 -1
  21. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/server.py +4 -1
  22. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/task.py +10 -4
  23. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/vm/pool.py +11 -7
  24. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/vm/rootfs.py +47 -34
  25. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/uv.lock +1 -1
  26. nightshift_sdk-0.4.0/.claude/settings.local.json +0 -25
  27. nightshift_sdk-0.4.0/infra/.instance +0 -4
  28. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/.claude/skills/opentui +0 -0
  29. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/CLAUDE.md +0 -0
  30. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/LICENSE +0 -0
  31. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/README.md +0 -0
  32. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/administration/api-keys.mdx +0 -0
  33. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/administration/backups.mdx +0 -0
  34. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/contributing/deploying-changes.mdx +0 -0
  35. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/contributing/dev-setup.mdx +0 -0
  36. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/deployment/architecture.mdx +0 -0
  37. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/deployment/cost-management.mdx +0 -0
  38. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/deployment/server.mdx +0 -0
  39. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/deployment/verify.mdx +0 -0
  40. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/docs.json +0 -0
  41. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/introduction.mdx +0 -0
  42. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/logo/kokapo.gif +0 -0
  43. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/logo/kokapo.svg +0 -0
  44. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/logo/nightshift-text.png +0 -0
  45. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/sdk/agent-config.mdx +0 -0
  46. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/docs/sdk/quickstart.mdx +0 -0
  47. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/basic_agent.py +0 -0
  48. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/code_reviewer.py +0 -0
  49. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/edit_agent.py +0 -0
  50. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/hooks_agent.py +0 -0
  51. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/mcp_agent.py +0 -0
  52. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/multi_agent.py +0 -0
  53. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/net_test.py +0 -0
  54. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/pyproject.toml +0 -0
  55. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/smoke_test.py +0 -0
  56. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/stateful_agent.py +0 -0
  57. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/test-workspace/hello.txt +0 -0
  58. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/examples/web_researcher.py +0 -0
  59. {nightshift_sdk-0.4.0/infra → nightshift_sdk-0.4.2/infra/dev}/.gitignore +0 -0
  60. {nightshift_sdk-0.4.0/infra → nightshift_sdk-0.4.2/infra/dev}/lambda/start.py +0 -0
  61. {nightshift_sdk-0.4.0/infra → nightshift_sdk-0.4.2/infra/dev}/lambda/stop.py +0 -0
  62. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/__init__.py +0 -0
  63. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/__main__.py +0 -0
  64. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/agent/__init__.py +0 -0
  65. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/agent/__main__.py +0 -0
  66. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/auth.py +0 -0
  67. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/cli/__init__.py +0 -0
  68. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/cli/commands/__init__.py +0 -0
  69. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/cli/commands/agents.py +0 -0
  70. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/cli/commands/api_key.py +0 -0
  71. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/cli/commands/deploy.py +0 -0
  72. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/cli/commands/login.py +0 -0
  73. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/cli/commands/logs.py +0 -0
  74. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/cli/config.py +0 -0
  75. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/cli/main.py +0 -0
  76. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/events.py +0 -0
  77. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/protocol/__init__.py +0 -0
  78. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/protocol/events.py +0 -0
  79. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/protocol/packaging.py +0 -0
  80. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/registry.py +0 -0
  81. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/sdk/__init__.py +0 -0
  82. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/sdk/app.py +0 -0
  83. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/sdk/config.py +0 -0
  84. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/vm/__init__.py +0 -0
  85. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/vm/manager.py +0 -0
  86. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/src/nightshift/vm/network.py +0 -0
  87. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/tests/__init__.py +0 -0
  88. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/tests/test_config.py +0 -0
  89. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/tests/test_deploy_api.py +0 -0
  90. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/tests/test_deploy_cli.py +0 -0
  91. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/tests/test_events.py +0 -0
  92. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/tests/test_protocol.py +0 -0
  93. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/tests/test_registry.py +0 -0
  94. {nightshift_sdk-0.4.0 → nightshift_sdk-0.4.2}/tests/test_sdk.py +0 -0
@@ -0,0 +1,79 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build-python:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+
13
+ - uses: astral-sh/setup-uv@v4
14
+
15
+ - run: uv build
16
+
17
+ - uses: actions/upload-artifact@v4
18
+ with:
19
+ name: dist
20
+ path: dist/
21
+
22
+ build-rootfs:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+
27
+ - name: Install build dependencies
28
+ run: sudo apt-get update && sudo apt-get install -y squashfs-tools wget
29
+
30
+ - name: Build base rootfs
31
+ run: sudo ./rootfs/build-rootfs.sh rootfs.ext4
32
+
33
+ - name: Bake agent runtime
34
+ run: sudo ./infra/bake-rootfs.sh rootfs.ext4
35
+
36
+ - name: Compress rootfs
37
+ run: gzip rootfs.ext4
38
+
39
+ - uses: actions/upload-artifact@v4
40
+ with:
41
+ name: rootfs
42
+ path: rootfs.ext4.gz
43
+
44
+ publish-pypi:
45
+ runs-on: ubuntu-latest
46
+ needs: [build-python]
47
+ environment: pypi
48
+ steps:
49
+ - uses: astral-sh/setup-uv@v4
50
+
51
+ - uses: actions/download-artifact@v4
52
+ with:
53
+ name: dist
54
+ path: dist/
55
+
56
+ - run: uv publish dist/*
57
+ env:
58
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
59
+
60
+ publish-github:
61
+ runs-on: ubuntu-latest
62
+ needs: [build-python, build-rootfs]
63
+ permissions:
64
+ contents: write
65
+ steps:
66
+ - uses: actions/download-artifact@v4
67
+ with:
68
+ name: dist
69
+ path: dist/
70
+
71
+ - uses: actions/download-artifact@v4
72
+ with:
73
+ name: rootfs
74
+
75
+ - uses: softprops/action-gh-release@v2
76
+ with:
77
+ files: |
78
+ dist/*
79
+ rootfs.ext4.gz
@@ -6,3 +6,4 @@ __pycache__/
6
6
  env/
7
7
  build/
8
8
  dist/
9
+ infra/.deploy-state
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nightshift-sdk
3
- Version: 0.4.0
3
+ Version: 0.4.2
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
@@ -4,24 +4,40 @@ sidebarTitle: "Provision"
4
4
  description: "Set up a c5.metal EC2 instance with Firecracker, KVM, and all dependencies."
5
5
  ---
6
6
 
7
+ ## Operator deploy
7
8
 
8
- ## Prerequisites
9
+ If you're an operator deploying Nightshift for your team, use the single-command deploy script. It provisions a c5.metal, pulls everything from PyPI and GitHub Releases, and starts serving — no repo checkout needed.
9
10
 
10
- - AWS CLI configured with credentials (EC2, IAM, Lambda, EventBridge Scheduler permissions)
11
- - `curl` and `jq` installed locally
12
- - The `infra/` directory from this repository
11
+ ```bash
12
+ ./infra/deploy.sh --hostname api.example.com --api-key ns_abc123
13
+ ```
14
+
15
+ Optional flags:
16
+ - `--region <region>` — AWS region (default: us-east-1)
17
+ - `--version <tag>` — pin to a specific release (default: latest)
18
+ - `--port <port>` — backend port (default: 3000)
19
+
20
+ The script allocates an Elastic IP and prints DNS instructions at the end. Point your hostname's A record at the Elastic IP, and Caddy provisions TLS automatically.
21
+
22
+ To upgrade an existing deployment:
23
+
24
+ ```bash
25
+ ./infra/upgrade.sh ubuntu@<ELASTIC_IP> --key ~/.ssh/nightshift.pem
26
+ ./infra/upgrade.sh --version v0.3.0 # reads target from state file
27
+ ```
28
+
29
+ ## Dev environment setup
13
30
 
14
- ## Run setup
31
+ For development (requires a local repo checkout):
15
32
 
16
33
  ```bash
17
- cd infra
18
- bash setup.sh
34
+ ./infra/dev/setup.sh
19
35
  ```
20
36
 
21
37
  For a production deployment with TLS in one step:
22
38
 
23
39
  ```bash
24
- bash setup.sh --production --hostname api.example.com
40
+ ./infra/dev/setup.sh --production --hostname api.example.com
25
41
  ```
26
42
 
27
43
  This opens ports 80/443, provisions the instance, and runs `production.sh` on the remote machine to install Caddy + systemd services. See [Server](/deployment/server) for details.
@@ -55,7 +71,7 @@ This opens ports 80/443, provisions the instance, and runs `production.sh` on th
55
71
  Polls SSH and waits for `/opt/nightshift/.setup-done` marker.
56
72
  </Step>
57
73
  <Step title="Write connection details">
58
- Writes connection details to `infra/.instance`.
74
+ Writes connection details to `infra/dev/.instance`.
59
75
  </Step>
60
76
  </Steps>
61
77
 
@@ -70,8 +86,7 @@ ssh -i ~/.ssh/nightshift-dev.pem ubuntu@<IP> 'tail -50 /var/log/nightshift-setup
70
86
  ## Tear down
71
87
 
72
88
  ```bash
73
- cd infra
74
- bash teardown.sh
89
+ ./infra/dev/teardown.sh
75
90
  ```
76
91
 
77
- Terminates the instance and removes `infra/.instance`. The SSH keypair and security group are preserved for next time.
92
+ Terminates the instance and removes `infra/dev/.instance`. The SSH keypair and security group are preserved for next time.
@@ -0,0 +1,22 @@
1
+ """OpenAI agent — simple streaming assistant.
2
+
3
+ Demonstrates Nightshift with the OpenAI Agents SDK.
4
+ Uses forward_env to pass the API key from the server, or --env from the CLI.
5
+
6
+ Usage:
7
+ nightshift deploy examples/oai_agent.py
8
+ nightshift run oai_agent -p "hello" -f --env OPENAI_API_KEY=$OPENAI_API_KEY
9
+ """
10
+
11
+ from agents import Agent, Runner
12
+ from nightshift import NightshiftApp, AgentConfig
13
+
14
+ app = NightshiftApp()
15
+
16
+
17
+ @app.agent(AgentConfig(workspace="", forward_env=["OPENAI_API_KEY"], max_concurrent_vms=3))
18
+ async def oai_agent(prompt: str):
19
+ agent = Agent(name="Assistant", instructions="You are a helpful assistant")
20
+ result = Runner.run_streamed(agent, prompt)
21
+ async for event in result.stream_events():
22
+ yield event
@@ -76,53 +76,13 @@ echo " protocol/ → packaging.py, events.py"
76
76
  # Replace systemd with a minimal shell init. Systemd is too heavy for
77
77
  # Firecracker microVMs — it can panic or hang during boot. Our init
78
78
  # sets up the essentials (filesystems, network, env) and starts the agent.
79
+ #
80
+ # The init script lives in rootfs/init (single source of truth).
79
81
  echo "==> Installing init script"
80
82
  # /sbin/init is a symlink to ../lib/systemd/systemd — remove it first
81
83
  # so we don't overwrite the systemd binary.
82
84
  rm -f "$MOUNT_DIR/sbin/init"
83
- cat > "$MOUNT_DIR/sbin/init" << 'INIT'
84
- #!/bin/sh
85
- #
86
- # Nightshift VM init — PID 1 inside the Firecracker microVM.
87
- #
88
-
89
- # Mount essential filesystems (ignore errors if kernel already mounted them)
90
- mount -t proc proc /proc 2>/dev/null
91
- mount -t sysfs sysfs /sys 2>/dev/null
92
- mount -t devtmpfs devtmpfs /dev 2>/dev/null
93
- mount -t tmpfs tmpfs /tmp 2>/dev/null
94
- mount -t tmpfs tmpfs /run 2>/dev/null
95
-
96
- # Configure network from kernel command line
97
- # ip=GUEST::GATEWAY:MASK::eth0:off
98
- GUEST_IP=$(cat /proc/cmdline | tr ' ' '\n' | grep '^ip=' | cut -d= -f2 | cut -d: -f1)
99
- GATEWAY=$(cat /proc/cmdline | tr ' ' '\n' | grep '^ip=' | cut -d= -f2 | cut -d: -f3)
100
- MASK=$(cat /proc/cmdline | tr ' ' '\n' | grep '^ip=' | cut -d= -f2 | cut -d: -f4)
101
-
102
- if [ -n "$GUEST_IP" ]; then
103
- ip addr add "${GUEST_IP}/${MASK}" dev eth0 2>/dev/null
104
- ip link set eth0 up 2>/dev/null
105
- ip route add default via "$GATEWAY" 2>/dev/null
106
- fi
107
-
108
- # DNS
109
- echo "nameserver 8.8.8.8" > /etc/resolv.conf
110
- echo "nameserver 8.8.4.4" >> /etc/resolv.conf
111
-
112
- # Load environment
113
- if [ -f /etc/nightshift/env ]; then
114
- set -a
115
- . /etc/nightshift/env
116
- set +a
117
- fi
118
-
119
- export PATH="/root/.local/bin:/usr/local/bin:/usr/bin:/bin"
120
- export PYTHONPATH="/opt"
121
-
122
- # Start the agent
123
- cd /workspace
124
- exec python3 -m nightshift.agent
125
- INIT
85
+ cp "$PROJECT_DIR/rootfs/init" "$MOUNT_DIR/sbin/init"
126
86
  chmod +x "$MOUNT_DIR/sbin/init"
127
87
 
128
88
  # ── Required directories ─────────────────────────────────────
@@ -0,0 +1,362 @@
1
+ #!/bin/bash
2
+ #
3
+ # Deploy Nightshift for operators.
4
+ #
5
+ # Provisions a c5.metal EC2 instance and bootstraps a production-ready
6
+ # Nightshift server. Everything is pulled from PyPI (nightshift-sdk) and
7
+ # GitHub Releases (rootfs.ext4.gz) — no local repo checkout needed.
8
+ #
9
+ # Prerequisites:
10
+ # - AWS CLI configured with EC2/VPC permissions
11
+ # - A hostname with DNS pointing to the Elastic IP (printed at the end)
12
+ #
13
+ # Usage:
14
+ # ./infra/deploy.sh --hostname api.example.com --api-key ns_abc123
15
+ # ./infra/deploy.sh --hostname api.example.com --api-key ns_abc123 --region us-west-2
16
+ # ./infra/deploy.sh --hostname api.example.com --api-key ns_abc123 --version v0.2.0
17
+ #
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
21
+ STATE_FILE="$SCRIPT_DIR/.deploy-state"
22
+
23
+ INSTANCE_TYPE="c5.metal"
24
+ KEY_NAME="nightshift"
25
+ SG_NAME="nightshift"
26
+ REGION="${AWS_DEFAULT_REGION:-us-east-1}"
27
+ HOSTNAME=""
28
+ API_KEY=""
29
+ PORT=3000
30
+ VERSION=""
31
+ GITHUB_REPO="nightshiftco/nightshift"
32
+
33
+ # Parse arguments
34
+ while [[ $# -gt 0 ]]; do
35
+ case $1 in
36
+ --hostname) HOSTNAME="$2"; shift 2 ;;
37
+ --api-key) API_KEY="$2"; shift 2 ;;
38
+ --region) REGION="$2"; shift 2 ;;
39
+ --version) VERSION="$2"; shift 2 ;;
40
+ --port) PORT="$2"; shift 2 ;;
41
+ *)
42
+ echo "Unknown option: $1"
43
+ echo "Usage: $0 --hostname <FQDN> --api-key <key> [--region <region>] [--version <tag>] [--port <port>]"
44
+ exit 1
45
+ ;;
46
+ esac
47
+ done
48
+
49
+ if [ -z "$HOSTNAME" ]; then
50
+ echo "Error: --hostname is required"
51
+ echo "Usage: $0 --hostname <FQDN> --api-key <key> [--region <region>] [--version <tag>] [--port <port>]"
52
+ exit 1
53
+ fi
54
+
55
+ if [ -z "$API_KEY" ]; then
56
+ echo "Error: --api-key is required"
57
+ echo "Generate one with: python3 -c \"import secrets; print(f'ns_{secrets.token_hex(16)}')\""
58
+ exit 1
59
+ fi
60
+
61
+ # If no version specified, fetch the latest release tag from GitHub.
62
+ if [ -z "$VERSION" ]; then
63
+ VERSION=$(curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | jq -r '.tag_name')
64
+ if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
65
+ echo "Error: could not determine latest release from GitHub"
66
+ exit 1
67
+ fi
68
+ fi
69
+
70
+ echo "==> Nightshift operator deploy"
71
+ echo " Hostname: $HOSTNAME"
72
+ echo " Version: $VERSION"
73
+ echo " Region: $REGION"
74
+ echo " Port: $PORT"
75
+ echo ""
76
+
77
+ # SSH key pair
78
+ if ! aws ec2 describe-key-pairs --key-names "$KEY_NAME" --region "$REGION" &>/dev/null; then
79
+ echo "==> Creating key pair: $KEY_NAME"
80
+ aws ec2 create-key-pair \
81
+ --key-name "$KEY_NAME" \
82
+ --region "$REGION" \
83
+ --query 'KeyMaterial' \
84
+ --output text > ~/.ssh/${KEY_NAME}.pem
85
+ chmod 400 ~/.ssh/${KEY_NAME}.pem
86
+ echo " Saved to ~/.ssh/${KEY_NAME}.pem"
87
+ else
88
+ echo "==> Key pair '$KEY_NAME' already exists in AWS"
89
+ if [ ! -f ~/.ssh/${KEY_NAME}.pem ]; then
90
+ echo " ERROR: Private key not found at ~/.ssh/${KEY_NAME}.pem"
91
+ echo " The key pair exists in AWS but the local private key is missing."
92
+ echo " Fix: delete the key pair in AWS and re-run this script."
93
+ echo " aws ec2 delete-key-pair --key-name $KEY_NAME --region $REGION"
94
+ exit 1
95
+ fi
96
+ fi
97
+
98
+ # Security group
99
+ SG_ID=$(aws ec2 describe-security-groups \
100
+ --filters "Name=group-name,Values=$SG_NAME" \
101
+ --region "$REGION" \
102
+ --query 'SecurityGroups[0].GroupId' \
103
+ --output text 2>/dev/null || echo "None")
104
+
105
+ if [ "$SG_ID" = "None" ] || [ -z "$SG_ID" ]; then
106
+ echo "==> Creating security group: $SG_NAME"
107
+ SG_ID=$(aws ec2 create-security-group \
108
+ --group-name "$SG_NAME" \
109
+ --description "Nightshift production - SSH, HTTP, HTTPS" \
110
+ --region "$REGION" \
111
+ --query 'GroupId' \
112
+ --output text)
113
+ fi
114
+
115
+ MY_IP=$(curl -s https://checkip.amazonaws.com)
116
+ echo "==> Updating security group rules"
117
+
118
+ # Remove stale SSH rules
119
+ OLD_RULES=$(aws ec2 describe-security-groups \
120
+ --group-ids "$SG_ID" \
121
+ --region "$REGION" \
122
+ --query 'SecurityGroups[0].IpPermissions[?FromPort==`22`].IpRanges[].CidrIp' \
123
+ --output text 2>/dev/null || echo "")
124
+ for cidr in $OLD_RULES; do
125
+ aws ec2 revoke-security-group-ingress \
126
+ --group-id "$SG_ID" \
127
+ --protocol tcp --port 22 --cidr "$cidr" \
128
+ --region "$REGION" &>/dev/null || true
129
+ done
130
+
131
+ # SSH from operator IP + HTTP/HTTPS from anywhere
132
+ aws ec2 authorize-security-group-ingress \
133
+ --group-id "$SG_ID" \
134
+ --protocol tcp --port 22 --cidr "${MY_IP}/32" \
135
+ --region "$REGION" &>/dev/null || true
136
+ aws ec2 authorize-security-group-ingress \
137
+ --group-id "$SG_ID" \
138
+ --protocol tcp --port 80 --cidr "0.0.0.0/0" \
139
+ --region "$REGION" &>/dev/null || true
140
+ aws ec2 authorize-security-group-ingress \
141
+ --group-id "$SG_ID" \
142
+ --protocol tcp --port 443 --cidr "0.0.0.0/0" \
143
+ --region "$REGION" &>/dev/null || true
144
+ echo " Security group: $SG_ID (SSH: ${MY_IP}/32, HTTP/HTTPS: 0.0.0.0/0)"
145
+
146
+ # Find latest Ubuntu 22.04 AMI
147
+ AMI_ID=$(aws ec2 describe-images \
148
+ --owners 099720109477 \
149
+ --filters "Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*" \
150
+ "Name=state,Values=available" \
151
+ --region "$REGION" \
152
+ --query 'sort_by(Images, &CreationDate)[-1].ImageId' \
153
+ --output text)
154
+ echo "==> AMI: $AMI_ID (Ubuntu 22.04 latest)"
155
+
156
+ # Build user-data bootstrap script
157
+ # Variable substitution: VERSION, PORT, API_KEY, HOSTNAME, GITHUB_REPO
158
+ # are injected into the heredoc. The rest of the script runs as root on
159
+ # first boot.
160
+
161
+ USERDATA=$(cat << USERDATA_EOF
162
+ #!/bin/bash
163
+ set -euxo pipefail
164
+ exec > >(tee /var/log/nightshift-setup.log) 2>&1
165
+
166
+ # ── System packages ──────────────────────────────────────────────────
167
+ apt-get update
168
+ apt-get upgrade -y
169
+ apt-get install -y curl wget jq acl iptables iproute2 rsync nodejs npm
170
+
171
+ # We install claude code for convenience on the server
172
+ echo "==> Installing claude CLI..."
173
+ npm install -g @anthropic-ai/claude-code
174
+
175
+ # ── KVM access ───────────────────────────────────────────────────────
176
+ modprobe kvm
177
+ modprobe kvm_intel 2>/dev/null || modprobe kvm_amd 2>/dev/null || true
178
+ chmod 666 /dev/kvm
179
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666"' > /etc/udev/rules.d/99-kvm.rules
180
+
181
+ # ── IP forwarding ────────────────────────────────────────────────────
182
+ echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-firecracker.conf
183
+ sysctl -p /etc/sysctl.d/99-firecracker.conf
184
+
185
+ # ── uv (system-wide to /root/.local/bin) ─────────────────────────────
186
+ curl -LsSf https://astral.sh/uv/install.sh | bash
187
+ export PATH="/root/.local/bin:\$PATH"
188
+
189
+ # ── Firecracker + jailer ─────────────────────────────────────────────
190
+ ARCH=\$(uname -m)
191
+ FC_VERSION=\$(curl -s https://api.github.com/repos/firecracker-microvm/firecracker/releases/latest | jq -r '.tag_name')
192
+ cd /tmp
193
+ curl -LO "https://github.com/firecracker-microvm/firecracker/releases/download/\${FC_VERSION}/firecracker-\${FC_VERSION}-\${ARCH}.tgz"
194
+ tar xzf "firecracker-\${FC_VERSION}-\${ARCH}.tgz"
195
+ mv "release-\${FC_VERSION}-\${ARCH}/firecracker-\${FC_VERSION}-\${ARCH}" /usr/local/bin/firecracker
196
+ mv "release-\${FC_VERSION}-\${ARCH}/jailer-\${FC_VERSION}-\${ARCH}" /usr/local/bin/jailer
197
+ chmod +x /usr/local/bin/firecracker /usr/local/bin/jailer
198
+
199
+ # ── Kernel (vmlinux) from Firecracker CI S3 bucket ───────────────────
200
+ mkdir -p /opt/nightshift
201
+ cd /opt/nightshift
202
+
203
+ CI_VERSION=\$(echo "\$FC_VERSION" | sed 's/\.[0-9]*\$//')
204
+ latest_kernel_key=\$(curl -s "http://spec.ccfc.min.s3.amazonaws.com/?prefix=firecracker-ci/\${CI_VERSION}/\${ARCH}/vmlinux-&list-type=2" \
205
+ | grep -oP "(?<=<Key>)(firecracker-ci/\${CI_VERSION}/\${ARCH}/vmlinux-[0-9]+\.[0-9]+\.[0-9]{1,3})(?=</Key>)" \
206
+ | sort -V | tail -1)
207
+ wget -q -O vmlinux "https://s3.amazonaws.com/spec.ccfc.min/\${latest_kernel_key}"
208
+
209
+ # ── rootfs from GitHub Release ───────────────────────────────────────
210
+ curl -L -o rootfs.ext4.gz "https://github.com/${GITHUB_REPO}/releases/download/${VERSION}/rootfs.ext4.gz"
211
+ gunzip rootfs.ext4.gz
212
+
213
+ # ── Install nightshift from PyPI via uvx ─────────────────────────────
214
+ # uvx installs and runs CLI tools from PyPI in isolated environments.
215
+ # Pin to the release version so operator knows exactly what's running.
216
+ SDK_VERSION=\$(echo "${VERSION}" | sed 's/^v//')
217
+ uvx --from "nightshift-sdk==\${SDK_VERSION}" nightshift --help
218
+
219
+ # ── Caddy (auto-TLS reverse proxy) ──────────────────────────────────
220
+ apt-get install -y debian-keyring debian-archive-keyring apt-transport-https
221
+ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
222
+ | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
223
+ curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
224
+ | tee /etc/apt/sources.list.d/caddy-stable.list
225
+ apt-get update -y
226
+ apt-get install -y caddy
227
+
228
+ cat > /etc/caddy/Caddyfile << 'CADDY'
229
+ ${HOSTNAME} {
230
+ reverse_proxy localhost:${PORT}
231
+ }
232
+ CADDY
233
+
234
+ # ── systemd service for nightshift serve ─────────────────────────────
235
+ cat > /etc/systemd/system/nightshift-serve.service << 'SYSTEMD'
236
+ [Unit]
237
+ Description=Nightshift Platform Server
238
+ After=network.target
239
+
240
+ [Service]
241
+ Type=simple
242
+ User=root
243
+ ExecStart=/root/.local/bin/uvx --from nightshift-sdk nightshift serve --port ${PORT}
244
+ Restart=on-failure
245
+ RestartSec=5
246
+ Environment=NIGHTSHIFT_API_KEY=${API_KEY}
247
+
248
+ [Install]
249
+ WantedBy=multi-user.target
250
+ SYSTEMD
251
+
252
+ # ── Enable and start services ────────────────────────────────────────
253
+ systemctl daemon-reload
254
+ systemctl enable --now nightshift-serve
255
+ systemctl enable caddy
256
+ systemctl reload caddy
257
+
258
+ # ── Done marker ──────────────────────────────────────────────────────
259
+ echo "setup_complete \$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /opt/nightshift/.setup-done
260
+ USERDATA_EOF
261
+ )
262
+
263
+ # Launch instance
264
+ echo "==> Launching $INSTANCE_TYPE instance..."
265
+ INSTANCE_ID=$(aws ec2 run-instances \
266
+ --image-id "$AMI_ID" \
267
+ --instance-type "$INSTANCE_TYPE" \
268
+ --key-name "$KEY_NAME" \
269
+ --security-group-ids "$SG_ID" \
270
+ --region "$REGION" \
271
+ --block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":100,"VolumeType":"gp3","DeleteOnTermination":true}}]' \
272
+ --user-data "$USERDATA" \
273
+ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=nightshift},{Key=Project,Value=nightshift},{Key=Version,Value=$VERSION}]" \
274
+ --query 'Instances[0].InstanceId' \
275
+ --output text)
276
+ echo " Instance ID: $INSTANCE_ID"
277
+
278
+ echo "==> Waiting for instance to enter 'running' state..."
279
+ aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" --region "$REGION"
280
+
281
+ # Allocate and associate Elastic IP
282
+ echo "==> Allocating Elastic IP..."
283
+ ALLOC_ID=$(aws ec2 allocate-address \
284
+ --domain vpc \
285
+ --region "$REGION" \
286
+ --tag-specifications "ResourceType=elastic-ip,Tags=[{Key=Name,Value=nightshift},{Key=Project,Value=nightshift}]" \
287
+ --query 'AllocationId' \
288
+ --output text)
289
+
290
+ ELASTIC_IP=$(aws ec2 describe-addresses \
291
+ --allocation-ids "$ALLOC_ID" \
292
+ --region "$REGION" \
293
+ --query 'Addresses[0].PublicIp' \
294
+ --output text)
295
+
296
+ aws ec2 associate-address \
297
+ --instance-id "$INSTANCE_ID" \
298
+ --allocation-id "$ALLOC_ID" \
299
+ --region "$REGION" \
300
+ --output text > /dev/null
301
+ echo " Elastic IP: $ELASTIC_IP"
302
+
303
+ # state
304
+ cat > "$STATE_FILE" << EOF
305
+ INSTANCE_ID=$INSTANCE_ID
306
+ ELASTIC_IP=$ELASTIC_IP
307
+ ALLOC_ID=$ALLOC_ID
308
+ KEY_PATH=~/.ssh/${KEY_NAME}.pem
309
+ REGION=$REGION
310
+ HOSTNAME=$HOSTNAME
311
+ VERSION=$VERSION
312
+ EOF
313
+ echo " State saved to $STATE_FILE"
314
+
315
+ # Wait for SSH
316
+ echo "==> Waiting for SSH..."
317
+ for i in $(seq 1 60); do
318
+ if ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o BatchMode=yes \
319
+ -i ~/.ssh/${KEY_NAME}.pem ubuntu@${ELASTIC_IP} "echo ok" &>/dev/null; then
320
+ echo " SSH is up"
321
+ break
322
+ fi
323
+ if [ "$i" -eq 60 ]; then
324
+ echo " ERROR: SSH did not come up after 10 minutes"
325
+ exit 1
326
+ fi
327
+ sleep 10
328
+ done
329
+
330
+ # Wait for user-data setup to complete
331
+ echo "==> Waiting for instance setup to complete..."
332
+ echo " (installing firecracker, kernel, rootfs, nightshift, caddy)"
333
+ for i in $(seq 1 90); do
334
+ DONE=$(ssh -o StrictHostKeyChecking=no -i ~/.ssh/${KEY_NAME}.pem \
335
+ ubuntu@${ELASTIC_IP} "cat /opt/nightshift/.setup-done 2>/dev/null" 2>/dev/null || echo "")
336
+ if [ -n "$DONE" ]; then
337
+ echo " $DONE"
338
+ break
339
+ fi
340
+ if [ "$i" -eq 90 ]; then
341
+ echo " ERROR: Setup did not complete after 15 minutes"
342
+ echo " Debug: ssh -i ~/.ssh/${KEY_NAME}.pem ubuntu@${ELASTIC_IP} 'tail -50 /var/log/nightshift-setup.log'"
343
+ exit 1
344
+ fi
345
+ sleep 10
346
+ done
347
+
348
+ # Print connection info
349
+ echo ""
350
+ echo "=== Nightshift deployment complete ==="
351
+ echo ""
352
+ echo " Elastic IP: $ELASTIC_IP"
353
+ echo " Server: https://$HOSTNAME"
354
+ echo " Health: https://$HOSTNAME/health"
355
+ echo " SSH: ssh -i ~/.ssh/${KEY_NAME}.pem ubuntu@${ELASTIC_IP}"
356
+ echo " Version: $VERSION"
357
+ echo ""
358
+ echo " DNS: Create an A record pointing $HOSTNAME to $ELASTIC_IP"
359
+ echo " Caddy will automatically provision TLS once DNS propagates."
360
+ echo ""
361
+ echo " Logs: ssh -i ~/.ssh/${KEY_NAME}.pem ubuntu@${ELASTIC_IP} 'sudo journalctl -u nightshift-serve -f'"
362
+ echo " Upgrade: ./infra/upgrade.sh ubuntu@${ELASTIC_IP} --key ~/.ssh/${KEY_NAME}.pem"
@@ -10,10 +10,10 @@
10
10
  # - EventBridge Schedules: start 8am ET Mon-Fri, stop midnight ET Tue-Sat
11
11
  #
12
12
  # Prerequisites:
13
- # - infra/.instance must exist (run setup.sh first)
13
+ # - infra/dev/.instance must exist (run setup.sh first)
14
14
  # - AWS CLI configured with appropriate permissions
15
15
  #
16
- # Usage: ./infra/cron.sh
16
+ # Usage: ./infra/dev/cron.sh
17
17
  #
18
18
  set -euo pipefail
19
19
 
@@ -7,14 +7,14 @@
7
7
  # next run cold-starts with the new code.
8
8
  #
9
9
  # Usage:
10
- # ./infra/deploy.sh # uses IP from .instance
11
- # ./infra/deploy.sh ubuntu@100.49.58.14 # explicit host
12
- # ./infra/deploy.sh --key ~/.ssh/my.pem ubuntu@host
10
+ # ./infra/dev/deploy.sh # uses IP from .instance
11
+ # ./infra/dev/deploy.sh ubuntu@100.49.58.14 # explicit host
12
+ # ./infra/dev/deploy.sh --key ~/.ssh/my.pem ubuntu@host
13
13
  #
14
14
  set -euo pipefail
15
15
 
16
16
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
- PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
17
+ PROJECT_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
18
18
  STATE_FILE="$SCRIPT_DIR/.instance"
19
19
 
20
20
  SSH_KEY=""
@@ -7,9 +7,9 @@
7
7
  # It does NOT handle DNS; point your hostname at this machine first.
8
8
  #
9
9
  # Usage:
10
- # ./infra/production.sh --hostname api.nightshift.sh
11
- # ./infra/production.sh --hostname api.nightshift.sh --port 8080
12
- # ./infra/production.sh --hostname api.nightshift.sh --api-key my-secret-key
10
+ # ./infra/dev/production.sh --hostname api.nightshift.sh
11
+ # ./infra/dev/production.sh --hostname api.nightshift.sh --port 8080
12
+ # ./infra/dev/production.sh --hostname api.nightshift.sh --api-key my-secret-key
13
13
  #
14
14
  set -euo pipefail
15
15
 
@@ -75,7 +75,7 @@ fi
75
75
  # Default to the directory containing this script's parent, but allow
76
76
  # override via NIGHTSHIFT_PROJECT_DIR.
77
77
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
78
- PROJECT_DIR="${NIGHTSHIFT_PROJECT_DIR:-$(dirname "$SCRIPT_DIR")}"
78
+ PROJECT_DIR="${NIGHTSHIFT_PROJECT_DIR:-$(dirname "$(dirname "$SCRIPT_DIR")")}"
79
79
 
80
80
  # Verify uv is available
81
81
  UV_PATH="$(command -v uv 2>/dev/null || echo "$HOME/.local/bin/uv")"
@@ -129,7 +129,7 @@ echo " Created /etc/caddy/Caddyfile"
129
129
  # 5. Bake agent runtime into rootfs
130
130
  # -------------------------------------------------------------------
131
131
  echo "==> Baking agent runtime into rootfs"
132
- sudo "$SCRIPT_DIR/bake-rootfs.sh"
132
+ sudo "$SCRIPT_DIR/../bake-rootfs.sh"
133
133
 
134
134
  # -------------------------------------------------------------------
135
135
  # 6. Enable and start services