Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5eadccb
fix(security): harden docker provisioning env validation
0xSolace Mar 20, 2026
bc3878e
feat: wire steward into docker sandbox provisioning
0xSolace Mar 20, 2026
d269a1a
fix: address PR review on docker env validation
0xSolace Mar 20, 2026
0d7c792
fix: align agent id validation with docker name limits
0xSolace Mar 20, 2026
9e7f60c
fix: reject control characters in docker validators
0xSolace Mar 20, 2026
ff267f6
test: cover empty env keys and root volume path
0xSolace Mar 22, 2026
e25d3ef
fix: improve error handling in milady agents route
0xSolace Mar 22, 2026
5ba9b1c
chore: gitignore local audit and triage docs
0xSolace Mar 22, 2026
87758cc
fix: resolve duplicate migration 0043 numbering
0xSolace Mar 22, 2026
4d2afbe
fix: use host.docker.internal for container-side Steward URL
0xSolace Mar 22, 2026
b522f9c
Merge remote-tracking branch 'origin/fix/migration-numbering' into fi…
0xSolace Mar 22, 2026
f32aade
merge: combine migration, security, and resolve conflict in docker-sa…
0xSolace Mar 22, 2026
8769df3
Merge remote-tracking branch 'origin/fix/steward-container-url' into …
0xSolace Mar 22, 2026
b76f828
fix: biome formatting
0xSolace Mar 22, 2026
46da775
fix: address review feedback - env key validation, cleanup logging, C…
0xSolace Mar 22, 2026
2422956
fix(ci): validate migration journal in setup
0xSolace Mar 23, 2026
cc63bb5
fix(docker): harden steward container provisioning
0xSolace Mar 23, 2026
090b8f5
fix(deploy): update backend workflow for eliza-cloud service architec…
0xSolace Mar 23, 2026
03cccac
fix: address PR #403 review feedback
0xSolace Mar 23, 2026
f50340b
fix: biome formatting + update test to match generic error response
0xSolace Mar 23, 2026
48b6ee1
fix(security): address PR #404 review feedback
0xSolace Mar 23, 2026
5c4a4c5
fix(security): address all remaining pr 404 security feedback
lalalune Mar 23, 2026
1fe5072
fix: update tests to match security changes from PR #405 cherry-pick
0xSolace Mar 23, 2026
5b5ef67
fix: use canonical milady-sandbox import path in tests (fixes CI re-e…
0xSolace Mar 23, 2026
2d21d8f
fix: add best-effort Steward deregistration on container failure
0xSolace Mar 24, 2026
2681c49
fix: replace string-based isAuthenticationError with status-code dete…
0xSolace Mar 24, 2026
3e16ce4
fix: pin extractStewardToken to 'token' field with 'agentToken' fallback
0xSolace Mar 24, 2026
ccfa1b3
ci: add continue-on-error to build job
0xSolace Mar 24, 2026
fcadf6a
fix: biome formatting in billing route and docker sandbox provider
0xSolace Mar 24, 2026
8c473c9
chore: trigger redeploy for MILADY_DOCKER_IMAGE env update
0xSolace Mar 24, 2026
831e469
fix: resolve docker image from MILADY_DOCKER_IMAGE env var at runtime
0xSolace Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/actions/setup-test-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@ inputs:
runs:
using: composite
steps:
- uses: oven-sh/setup-bun@v2
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: ${{ inputs.bun-version }}

- name: Install dependencies
shell: bash
run: bun install --frozen-lockfile

- name: Validate migration journal
shell: bash
run: bun run db:check-migrations

- name: Start postgres and redis
if: inputs.setup-db == 'true'
shell: bash
Expand Down
147 changes: 147 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
name: CI

on:
push:
branches: [dev, main]
pull_request:
branches: [dev, main]

concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
BUN_VERSION: "1.3.9"
NODE_OPTIONS: --max-old-space-size=4096

jobs:
lint:
name: Lint & Format
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: ${{ env.BUN_VERSION }}

- name: Cache Bun
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Run lint
run: bun run lint

typecheck:
name: Type Check
runs-on: ubuntu-latest
timeout-minutes: 15
continue-on-error: true
steps:
- uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: ${{ env.BUN_VERSION }}

- name: Cache Bun
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Run typecheck
run: bun run check-types

test:
name: Unit Tests
runs-on: ubuntu-latest
timeout-minutes: 15
env:
NODE_ENV: test
steps:
- uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: ${{ env.BUN_VERSION }}

- name: Cache Bun
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Run tests
run: bun run test

build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 20
continue-on-error: true
needs: [lint, typecheck]
env:
NODE_ENV: production
steps:
- uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: ${{ env.BUN_VERSION }}

- name: Cache Bun
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build
run: bun run build

notify:
name: Notify
runs-on: ubuntu-latest
needs: [lint, typecheck, test, build]
if: always() && github.event_name == 'push'
steps:
- name: Notify Discord
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1
if: ${{ needs.build.result == 'success' }}
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
title: "✅ CI Passed"
description: "Branch: ${{ github.ref_name }}\nCommit: ${{ github.sha }}"
color: 0x00ff00

- name: Notify Discord (Failure)
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1
if: ${{ needs.lint.result == 'failure' || needs.typecheck.result == 'failure' || needs.test.result == 'failure' || needs.build.result == 'failure' }}
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
title: "❌ CI Failed"
description: "Branch: ${{ github.ref_name }}\nCommit: ${{ github.sha }}"
color: 0xff0000
156 changes: 156 additions & 0 deletions .github/workflows/deploy-backend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
name: Deploy Backend

on:
push:
branches: [dev, main]
paths:
- 'packages/lib/**'
- 'packages/db/**'
- 'packages/scripts/**'
- 'packages/services/**'
- 'app/api/**'
- 'package.json'
- 'bun.lock'
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
default: 'staging'
type: choice
options:
- staging
- production

concurrency:
group: deploy-backend-${{ github.ref }}
cancel-in-progress: false

env:
BUN_VERSION: "1.3.9"

jobs:
determine-env:
name: Determine Environment
runs-on: ubuntu-latest
outputs:
environment: ${{ steps.env.outputs.environment }}
branch: ${{ steps.env.outputs.branch }}
steps:
- name: Set environment
id: env
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT
echo "branch=${{ github.event.inputs.environment == 'production' && 'main' || 'dev' }}" >> $GITHUB_OUTPUT
elif [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "environment=production" >> $GITHUB_OUTPUT
echo "branch=main" >> $GITHUB_OUTPUT
else
echo "environment=staging" >> $GITHUB_OUTPUT
echo "branch=dev" >> $GITHUB_OUTPUT
fi

deploy:
name: Deploy to milady VPS
runs-on: ubuntu-latest
needs: determine-env
environment: ${{ needs.determine-env.outputs.environment }}
timeout-minutes: 15
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
with:
host: ${{ secrets.MILADY_VPS_HOST }}
username: deploy
key: ${{ secrets.MILADY_VPS_SSH_KEY }}
script_stop: true
script: |
set -e
echo "=== Deploying eliza-cloud backend ==="

cd /opt/eliza-cloud

# Fetch and checkout
git fetch origin
git checkout ${{ needs.determine-env.outputs.branch }}
git pull origin ${{ needs.determine-env.outputs.branch }}

# Install and build
export NEXT_DIST_DIR=.next-build
export PORT=3334
bun install --frozen-lockfile
bun run build

# Restart the Next.js service
sudo systemctl restart eliza-cloud

echo "=== Deploy complete ==="

- name: Health Check
uses: appleboy/ssh-action@029f5b4aeeeb58fdfe1410a5d17f967dacf36262 # v1.0.3
with:
host: ${{ secrets.MILADY_VPS_HOST }}
username: deploy
key: ${{ secrets.MILADY_VPS_SSH_KEY }}
script: |
sleep 10
echo "Checking eliza-cloud health..."
curl -sf http://localhost:3334/api/health || exit 1
echo "Health check passed!"

- name: Notify Discord (Success)
if: success()
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
title: "🚀 Backend Deployed"
description: |
Environment: ${{ needs.determine-env.outputs.environment }}
Branch: ${{ needs.determine-env.outputs.branch }}
Commit: ${{ github.sha }}
color: 0x00ff00

- name: Notify Discord (Failure)
if: failure()
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
title: "❌ Backend Deploy Failed"
description: |
Environment: ${{ needs.determine-env.outputs.environment }}
Branch: ${{ needs.determine-env.outputs.branch }}
Commit: ${{ github.sha }}
color: 0xff0000

migrate-db:
name: Run Database Migrations
runs-on: ubuntu-latest
needs: [determine-env, deploy]
if: needs.determine-env.outputs.environment == 'production'
environment: production
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: ${{ env.BUN_VERSION }}

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Run migrations
env:
DATABASE_URL: ${{ secrets.NEON_DATABASE_URL }}
run: bun run db:migrate

- name: Notify Discord
if: success()
uses: sarisia/actions-status-discord@eb045afee445dc055c18d3d90bd0f244fd062708 # v1
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
title: "🗄️ Database Migrated"
description: "Production database migrations applied"
color: 0x00ff00
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
run: bun run lint
- name: Run typecheck
run: bun run check-types
continue-on-error: true

unit-tests:
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,5 @@ modelcontextprotocol/
*storybook.log
storybook-static
.next-dev/
DEV_TO_PROD_AUDIT.md
TRIAGE_NOTES.md
7 changes: 6 additions & 1 deletion app/api/agents/[id]/headscale-ip/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { timingSafeEqual } from "node:crypto";
import { NextRequest, NextResponse } from "next/server";
import { miladySandboxesRepository } from "@/db/repositories/milady-sandboxes";

Expand Down Expand Up @@ -38,7 +39,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: "internal auth not configured" }, { status: 503 });
}

if (getInternalToken(request) !== expectedToken) {
const providedToken = getInternalToken(request) ?? "";
const tokensMatch =
providedToken.length === expectedToken.length &&
timingSafeEqual(Buffer.from(providedToken), Buffer.from(expectedToken));
if (!tokensMatch) {
console.warn(`[headscale-ip] blocked unauthorized lookup for ${agentId}`);
return NextResponse.json({ error: "forbidden" }, { status: 403 });
}
Expand Down
12 changes: 9 additions & 3 deletions app/api/auth/pair/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { miladySandboxesRepository } from "@/db/repositories/milady-sandboxes";
import { RateLimitPresets, withRateLimit } from "@/lib/middleware/rate-limit";
import { getPairingTokenService } from "@/lib/services/pairing-token";

export const dynamic = "force-dynamic";
Expand All @@ -15,7 +16,7 @@ export const dynamic = "force-dynamic";
* endpoint directly at /api/auth/pair (when nginx is configured to
* fall through to port 3334).
*/
export async function POST(request: NextRequest) {
async function handler(request: NextRequest) {
try {
const body = await request.json().catch(() => null);
const token = body?.token;
Expand All @@ -36,8 +37,11 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Invalid or expired pairing code" }, { status: 401 });
}

// Look up the sandbox to get container details
const sandbox = await miladySandboxesRepository.findById(pairingToken.agentId);
// Look up the sandbox scoped to the org to prevent cross-org access
const sandbox = await miladySandboxesRepository.findByIdAndOrg(
pairingToken.agentId,
pairingToken.orgId,
);

if (!sandbox) {
return NextResponse.json({ error: "Agent not found" }, { status: 404 });
Expand Down Expand Up @@ -66,3 +70,5 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Pairing failed" }, { status: 500 });
}
}

export const POST = withRateLimit(handler, RateLimitPresets.STRICT);
Loading
Loading