Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
740dc5f
fix: bridge port should map to agent API port (2138), not unused dev …
0xSolace Mar 25, 2026
e7c31f1
Merge pull request #413 from elizaOS/fix/bridge-port-mapping
lalalune Mar 25, 2026
41a3bc3
chore: trigger deploy for bridge port fix
0xSolace Mar 25, 2026
ebd0e1e
infra: separate VPS provisioning from Vercel crons
0xSolace Mar 26, 2026
c6ccc23
feat: Phase 1 — WalletProvider abstraction + Steward dual-provider ro…
0xSolace Mar 26, 2026
7763cda
feat: expose Steward wallet info in agent APIs and admin dashboard
0xSolace Mar 26, 2026
8b9fd49
fix: compat agents map type mismatch + restore feature-flags exports
0xSolace Mar 26, 2026
3b3f3cd
feat: add wallet info section to agent detail page
0xSolace Mar 27, 2026
ad5a54a
test: add dual-provider wallet routing integration tests
0xSolace Mar 27, 2026
a3c5356
fix: resolve feature-flags TypeScript path resolution errors
0xSolace Mar 27, 2026
1b12ebf
fix: fall back to root UI when agent has no ui token
0xSolace Mar 27, 2026
40e1c4d
fix: remove direct container port exposure and client-side direct acc…
0xSolace Mar 27, 2026
608119d
fix: increase health check timeout to 180s (container start-period is…
0xSolace Mar 28, 2026
5bc67b4
fix(milady): wallet proxy, Neon branches, remove provisioning cron, d…
0xSolace Mar 30, 2026
9ec941c
fix(ci): biome lint/format fixes - unused vars, imports, formatting
0xSolace Mar 30, 2026
4be0db1
fix(billing): reuse Settings billing tab on /dashboard/billing page
0xSolace Mar 30, 2026
f44f93f
fix(ci): resolve 24 unit test failures caused by bun mock.module poll…
0xSolace Mar 30, 2026
29dcca1
fix(ci): fix compat-envelope domain assertion and mcp-tools credits m…
0xSolace Mar 30, 2026
9513fe4
fix(security): address 4 critical review items - path validation, que…
0xSolace Mar 30, 2026
98adc83
fix: restore Neon project ID fallback with type annotation to fix TS …
0xSolace Mar 30, 2026
34cea41
fix(ci): add missing UsersRepository and writeTransaction exports to …
0xSolace Mar 30, 2026
43b3e43
fix(ci): add resetWhatsAppColumnSupportCacheForTests to UsersReposito…
0xSolace Mar 30, 2026
79afb80
fix(ci): preserve real UsersRepository class in mocks to fix downstre…
0xSolace Mar 30, 2026
914360a
fix: use correct Neon parent project ID accessible by API key
0xSolace Mar 30, 2026
9d576d9
fix(provisioning): disable sync provision in production - always use …
0xSolace Mar 30, 2026
b95040f
fix: use primary DB for findRunningSandbox to avoid read replica lag
0xSolace Mar 30, 2026
1a52ef0
fix: add detailed logging to wallet proxy for debugging 503s
0xSolace Mar 30, 2026
f8936d4
fix: use public agent domain (waifu.fun) for wallet proxy instead of …
0xSolace Mar 30, 2026
b68d97b
fix: completely disable Vercel provisioning cron - VPS worker only
0xSolace Mar 30, 2026
0def87d
fix: use shared DATABASE_URL instead of per-agent Neon projects
0xSolace Mar 31, 2026
891f3cd
Merge remote-tracking branch 'origin/feat/steward-wallet-migration' i…
0xSolace Mar 31, 2026
1885bf2
merge: resolve conflicts taking deslop changes
0xSolace Mar 31, 2026
1e88fae
merge: consolidate PRs #414, #415, #418, #421 — VPS separation + Stew…
0xSolace Mar 31, 2026
88a8ad5
fix: update provision test to match consolidated error handling
0xSolace Mar 31, 2026
0f58e9e
fix: remove webUiUrl prop not in MiladyAgentActionsProps (merge artif…
0xSolace Mar 31, 2026
595a3ad
fix: address all review comments — cleanup stray docs, security harde…
0xSolace Mar 31, 2026
aa415c5
fix: restore real provisioning handler — stub broke VPS cron
0xSolace Mar 31, 2026
02544dd
fix: health check port mismatch — use PORT (2139) not MILADY_PORT (21…
0xSolace Mar 31, 2026
7e323e8
fix: stop clearing MILADY_API_TOKEN/ELIZA_API_TOKEN in Docker provider
0xSolace Mar 31, 2026
1f87b88
fix: use shared DATABASE_URL in user-database.ts (missed in PR #421)
0xSolace Apr 1, 2026
88ca374
fix: don't throw on first /api/auth/pair call — rate-limit warn not t…
0xSolace Apr 1, 2026
bdb2b5b
fix: update model defaults to Sonnet 4.6 + Minimax M2.7, refresh cata…
0xSolace Apr 2, 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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -739,3 +739,11 @@ ELIZA_APP_WHATSAPP_PHONE_NUMBER=
# Create a dedicated org + user in the platform for this purpose.
# WAIFU_SERVICE_ORG_ID=uuid-of-waifu-service-org
# WAIFU_SERVICE_USER_ID=uuid-of-waifu-service-user

# ── Steward Wallet Provider (Phase 1) ────────────────────────────────────
# Set USE_STEWARD_FOR_NEW_WALLETS=true to route new agent wallets through Steward
# instead of Privy. Existing wallets are unaffected.
# USE_STEWARD_FOR_NEW_WALLETS=false
# STEWARD_API_URL=http://localhost:3200
# STEWARD_TENANT_API_KEY=stw_your_key_here
# STEWARD_TENANT_ID=milady-cloud
3 changes: 3 additions & 0 deletions .github/workflows/deploy-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ jobs:
# Restart the Next.js service
sudo systemctl restart eliza-cloud

# Restart the provisioning worker (VPS owns container lifecycle)
sudo systemctl restart milady-provisioning-worker

echo "=== Deploy complete ==="

- name: Health Check
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ storybook-static
DEV_TO_PROD_AUDIT.md
TRIAGE_NOTES.md
.env.preview
.env.local.bak-*
66 changes: 0 additions & 66 deletions CLAUDE.md

This file was deleted.

19 changes: 18 additions & 1 deletion app/api/compat/agents/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@/lib/api/compat-envelope";
import { reusesExistingMiladyCharacter } from "@/lib/services/milady-agent-config";
import { miladySandboxService } from "@/lib/services/milady-sandbox";
import { getStewardAgent } from "@/lib/services/steward-client";
import { logger } from "@/lib/utils/logger";
import { requireCompatAuth } from "../../_lib/auth";
import { handleCompatCorsOptions, withCompatCors } from "../../_lib/cors";
Expand Down Expand Up @@ -41,7 +42,23 @@ export async function GET(request: NextRequest, { params }: RouteParams) {
);
}

return withCompatCors(NextResponse.json(envelope(toCompatAgent(agent))), CORS_METHODS);
// Resolve wallet info for Docker-backed agents
let walletInfo: { address: string | null; provider: "steward" | "privy" | null } | undefined;
if (agent.node_id) {
try {
const stewardAgent = await getStewardAgent(agentId);
if (stewardAgent?.walletAddress) {
walletInfo = { address: stewardAgent.walletAddress, provider: "steward" };
}
} catch {
// Steward unreachable — wallet fields will be null
}
}

return withCompatCors(
NextResponse.json(envelope(toCompatAgent(agent, walletInfo))),
CORS_METHODS,
);
} catch (err) {
return handleCompatError(err, CORS_METHODS);
}
Expand Down
2 changes: 1 addition & 1 deletion app/api/compat/agents/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function GET(request: NextRequest) {
try {
const { user } = await requireCompatAuth(request);
const agents = await miladySandboxService.listAgents(user.organization_id);
return withCompatCors(NextResponse.json(envelope(agents.map(toCompatAgent))), CORS_METHODS);
return withCompatCors(NextResponse.json(envelope(agents.map((a) => toCompatAgent(a)))), CORS_METHODS);
} catch (err) {
return handleCompatError(err, CORS_METHODS);
}
Expand Down
166 changes: 166 additions & 0 deletions app/api/cron/cleanup-stuck-provisioning/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/**
* Cleanup Stuck Provisioning Cron
*
* Detects and recovers agents that are stuck in "provisioning" status with no
* active job to drive them forward. This happens when:
*
* 1. A container crashes while the agent is running, and something (e.g.
* the Next.js sync provision path) sets status = 'provisioning' but
* never creates a jobs-table record.
* 2. A provision job is enqueued but the worker invocation dies before it
* can claim the record — in this case the job-recovery logic in
* process-provisioning-jobs will already handle it, but we add a belt-
* and-suspenders check here for the no-job case.
*
* Criteria for "stuck":
* - status = 'provisioning'
* - updated_at < NOW() - 10 minutes (well beyond any normal provision time)
* - no jobs row in ('pending', 'in_progress') whose data->>'agentId' matches
*
* Action: set status = 'error', write a descriptive error_message so the user
* can see what happened and re-provision.
*
* Schedule: every 5 minutes ("* /5 * * * *" in vercel.json)
* Protected by CRON_SECRET.
*/

import { and, eq, lt, sql } from "drizzle-orm";
import { NextRequest, NextResponse } from "next/server";
import { dbWrite } from "@/db/client";
import { jobs } from "@/db/schemas/jobs";
import { miladySandboxes } from "@/db/schemas/milady-sandboxes";
import { verifyCronSecret } from "@/lib/api/cron-auth";
import { logger } from "@/lib/utils/logger";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export const maxDuration = 60;

/** How long an agent must be stuck before we reset it (ms). */
const STUCK_THRESHOLD_MINUTES = 10;

interface CleanupResult {
agentId: string;
agentName: string | null;
organizationId: string;
stuckSinceMinutes: number;
}

async function handleCleanupStuckProvisioning(request: NextRequest) {
try {
const authError = verifyCronSecret(request, "[Cleanup Stuck Provisioning]");
if (authError) return authError;

logger.info("[Cleanup Stuck Provisioning] Starting scan");

const cutoff = new Date(Date.now() - STUCK_THRESHOLD_MINUTES * 60 * 1000);

/**
* Single UPDATE … RETURNING query:
*
* UPDATE milady_sandboxes
* SET status = 'error',
* error_message = '...',
* updated_at = NOW()
* WHERE status = 'provisioning'
* AND updated_at < :cutoff
* AND NOT EXISTS (
* SELECT 1 FROM jobs
* WHERE jobs.data->>'agentId' = milady_sandboxes.id::text
* AND jobs.status IN ('pending', 'in_progress')
* )
* RETURNING id, agent_name, organization_id, updated_at
*
* We run this inside dbWrite so it lands on the primary replica and is
* subject to the write path's connection pool.
*/
const stuckAgents = await dbWrite
.update(miladySandboxes)
.set({
status: "error",
error_message:
"Agent was stuck in provisioning state with no active provisioning job. " +
"This usually means a container crashed before the provisioning job could be created, " +
"or the job was lost. Please try starting the agent again.",
updated_at: new Date(),
})
.where(
and(
eq(miladySandboxes.status, "provisioning"),
lt(miladySandboxes.updated_at, cutoff),
sql`NOT EXISTS (
SELECT 1 FROM ${jobs}
WHERE ${jobs.data}->>'agentId' = ${miladySandboxes.id}::text
AND ${jobs.status} IN ('pending', 'in_progress')
)`,
),
)
.returning({
agentId: miladySandboxes.id,
agentName: miladySandboxes.agent_name,
organizationId: miladySandboxes.organization_id,
updatedAt: miladySandboxes.updated_at,
});

const results: CleanupResult[] = stuckAgents.map((row) => ({
agentId: row.agentId,
agentName: row.agentName,
organizationId: row.organizationId,
// updatedAt is now the new timestamp; we can't recover the old one here,
// but the log message below captures the count.
stuckSinceMinutes: STUCK_THRESHOLD_MINUTES, // minimum — actual may be longer
}));

if (results.length > 0) {
logger.warn("[Cleanup Stuck Provisioning] Reset stuck agents", {
count: results.length,
agents: results.map((r) => ({
agentId: r.agentId,
agentName: r.agentName,
organizationId: r.organizationId,
})),
});
} else {
logger.info("[Cleanup Stuck Provisioning] No stuck agents found");
}

return NextResponse.json({
success: true,
data: {
cleaned: results.length,
thresholdMinutes: STUCK_THRESHOLD_MINUTES,
timestamp: new Date().toISOString(),
agents: results,
},
});
} catch (error) {
logger.error(
"[Cleanup Stuck Provisioning] Failed:",
error instanceof Error ? error.message : String(error),
);

return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "Cleanup failed",
},
{ status: 500 },
);
}
}

/**
* GET /api/cron/cleanup-stuck-provisioning
* Cron endpoint — protected by CRON_SECRET (Vercel passes it automatically).
*/
export async function GET(request: NextRequest) {
return handleCleanupStuckProvisioning(request);
}

/**
* POST /api/cron/cleanup-stuck-provisioning
* Manual trigger for testing — same auth requirement.
*/
export async function POST(request: NextRequest) {
return handleCleanupStuckProvisioning(request);
}
2 changes: 2 additions & 0 deletions app/api/stripe/create-checkout-session/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const ALLOWED_ORIGINS = [
process.env.NEXT_PUBLIC_APP_URL,
"http://localhost:3000",
"http://localhost:3001",
"https://milady.ai",
"https://www.milady.ai",
].filter(Boolean) as string[];

// Configurable currency
Expand Down
56 changes: 55 additions & 1 deletion app/api/v1/admin/docker-containers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,33 @@ import { NextRequest, NextResponse } from "next/server";
import { dbRead } from "@/db/helpers";
import { type MiladySandboxStatus, miladySandboxes } from "@/db/schemas/milady-sandboxes";
import { requireAdmin } from "@/lib/auth";
import { getStewardAgent } from "@/lib/services/steward-client";
import { logger } from "@/lib/utils/logger";

export const dynamic = "force-dynamic";

const STEWARD_ENRICHMENT_CONCURRENCY = 5;

async function mapWithConcurrency<T, R>(
items: T[],
limit: number,
mapper: (item: T, index: number) => Promise<R>,
): Promise<R[]> {
const results = new Array<R>(items.length);
let nextIndex = 0;

async function worker() {
while (nextIndex < items.length) {
const currentIndex = nextIndex++;
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
}
}

const workerCount = Math.min(limit, items.length);
await Promise.all(Array.from({ length: workerCount }, () => worker()));
return results;
}

// ---------------------------------------------------------------------------
// GET — List all Docker containers across all nodes
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -97,10 +120,41 @@ export async function GET(request: NextRequest) {
.orderBy(desc(miladySandboxes.created_at))
.limit(limit);

// Enrich containers with wallet info from Steward (best-effort, parallel)
const enrichedContainers = await mapWithConcurrency(
containers,
STEWARD_ENRICHMENT_CONCURRENCY,
async (c) => {
let walletAddress: string | null = null;
let walletProvider: "steward" | "privy" | null = null;

// All Docker-node containers use Steward wallets
if (c.nodeId) {
try {
const stewardAgent = await getStewardAgent(c.id);
if (stewardAgent?.walletAddress) {
walletAddress = stewardAgent.walletAddress;
walletProvider = "steward";
} else {
walletProvider = "steward"; // registered but wallet pending
}
} catch {
// Steward unreachable — leave as null
}
}

return {
...c,
walletAddress,
walletProvider,
};
},
);

return NextResponse.json({
success: true,
data: {
containers,
containers: enrichedContainers,
total: totalCount, // actual total matching filters
returned: containers.length, // number returned in this page
filters: {
Expand Down
2 changes: 1 addition & 1 deletion app/api/v1/admin/service-pricing/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { servicePricingRepository } from "@/db/repositories";
import { servicePricingRepository } from "@/db/repositories/service-pricing";
import { requireAdminWithResponse } from "@/lib/api/admin-auth";
import { invalidateServicePricingCache } from "@/lib/services/proxy/pricing";
import { logger } from "@/lib/utils/logger";
Expand Down
Loading
Loading