fix traffic observability, tg alerting robustness and prod compose ports
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s

This commit is contained in:
OG T
2026-06-07 20:38:19 +08:00
parent 3b665e4cbe
commit 663ad4d3a2
6 changed files with 436 additions and 34 deletions

View File

@@ -190,6 +190,9 @@ export async function GET(request: NextRequest) {
totalRows,
latestEvents,
judgeCompleteRows,
demandPoolCount,
demandPoolNewRows,
demandPoolScopeStats,
] = await Promise.all([
prisma.auditEvent.groupBy({
by: ["action"],
@@ -247,8 +250,48 @@ export async function GET(request: NextRequest) {
metadata: true,
},
}),
prisma.task.count({
where: {
status: "OPEN",
title: {
not: {
startsWith: "GitHub Issue:",
},
},
},
}),
prisma.task.count({
where: {
status: "OPEN",
title: {
not: {
startsWith: "GitHub Issue:",
},
},
created_at: { gte: since },
},
}),
prisma.task.aggregate({
where: {
status: "OPEN",
title: {
not: {
startsWith: "GitHub Issue:",
},
},
},
_avg: {
scope_clarity_score: true,
},
}),
]);
const demandSupply = {
open_task_count: demandPoolCount,
new_open_tasks_in_window: demandPoolNewRows,
avg_scope_clarity: demandPoolScopeStats._avg.scope_clarity_score,
};
let capturedPayoutCount = 0;
let releasedPayoutCount = 0;
try {
@@ -429,9 +472,12 @@ export async function GET(request: NextRequest) {
latest_surface: string;
latest_source_ip: string;
latest_user_agent: string;
latest_task_id: string;
latest_response_status: number | null;
latest_response_summary: string;
latest_reason: string;
latest_payload_summary: string;
latest_request_id: string;
latest_created_at_ms: number;
}> = new Map();
@@ -471,13 +517,34 @@ export async function GET(request: NextRequest) {
latest_surface: normalizedSurface,
latest_source_ip: normalizedIp,
latest_user_agent: normalizedUa,
latest_task_id: taskId,
latest_response_status: responseStatus,
latest_response_summary: responseSummary,
latest_reason: event.reason || "unknown",
latest_payload_summary:
typeof metadata?.payload_summary === "string" ? metadata.payload_summary : "unknown",
latest_request_id:
typeof metadata?.request_id === "string" ? metadata.request_id : "unknown",
latest_created_at_ms: eventAt,
});
} else {
existingActorActivity.events += 1;
if (eventAt > existingActorActivity.latest_created_at_ms) {
existingActorActivity.latest_action = event.action;
existingActorActivity.latest_surface = normalizedSurface;
existingActorActivity.latest_source_ip = normalizedIp;
existingActorActivity.latest_user_agent = normalizedUa;
existingActorActivity.latest_task_id = taskId;
existingActorActivity.latest_response_status = responseStatus;
existingActorActivity.latest_response_summary = responseSummary;
existingActorActivity.latest_reason = event.reason || "unknown";
existingActorActivity.latest_payload_summary =
typeof metadata?.payload_summary === "string" ? metadata.payload_summary : "unknown";
existingActorActivity.latest_request_id =
typeof metadata?.request_id === "string" ? metadata.request_id : "unknown";
existingActorActivity.latest_created_at_ms = eventAt;
}
}
if (
@@ -566,6 +633,7 @@ export async function GET(request: NextRequest) {
period_minutes: minutes,
total_events: totalRows,
action_summary: actionSummary,
demand_supply: demandSupply,
channel_summary: channelSummary,
actor_summary: actorSummary,
external_actor_summary: externalActorSummary,

View File

@@ -98,9 +98,12 @@ type ExternalActorActivity = {
latestSurface: string;
latestSourceIp: string;
latestUserAgent: string;
latestTaskId: string;
latestResponseStatus: number | null;
latestResponseSummary: string;
latestReason: string;
latestPayloadSummary: string;
latestRequestId: string;
latestCreatedAt: number;
};
@@ -116,6 +119,9 @@ async function getTrafficSummary(minutes: number) {
judgeCompleteRows,
capturedPayoutCount,
releasedPayoutCount,
demandPoolCount,
demandPoolNewRows,
demandPoolScopeStats,
] = await Promise.all([
prisma.auditEvent.groupBy({
by: ["action"],
@@ -187,8 +193,48 @@ async function getTrafficSummary(minutes: number) {
response_status: "SUCCESS",
},
}),
prisma.task.count({
where: {
status: "OPEN",
title: {
not: {
startsWith: "GitHub Issue:",
},
},
},
}),
prisma.task.count({
where: {
status: "OPEN",
title: {
not: {
startsWith: "GitHub Issue:",
},
},
created_at: { gte: since },
},
}),
prisma.task.aggregate({
where: {
status: "OPEN",
title: {
not: {
startsWith: "GitHub Issue:",
},
},
},
_avg: {
scope_clarity_score: true,
},
}),
]);
const demandSupply = {
openTaskCount: demandPoolCount,
newOpenTasksInWindow: demandPoolNewRows,
avgScopeClarity: demandPoolScopeStats._avg.scope_clarity_score,
};
const actionSummary = Object.fromEntries(summaryRows.map((row) => [row.action, row._count._all]));
const actorSummary = Object.fromEntries(actorSummaryRows.map((row) => [row.actorType, row._count._all]));
const externalActorSummary = externalActorRows
@@ -228,6 +274,8 @@ async function getTrafficSummary(minutes: number) {
level: metadata?.level,
response_status: typeof metadata?.response_status === "number" ? metadata.response_status : null,
response_summary: typeof metadata?.response_summary === "string" ? metadata.response_summary : "unknown",
payload_summary: typeof metadata?.payload_summary === "string" ? metadata.payload_summary : "unknown",
request_id: typeof metadata?.request_id === "string" ? metadata.request_id : "n/a",
metadata,
};
});
@@ -267,6 +315,9 @@ async function getTrafficSummary(minutes: number) {
.filter((event) => event.action.includes("ERROR"))
.map((event) => event.action);
const demandHealthLabel = demandSupply.openTaskCount > 0 ? "有可接需求" : "無可接需求";
const demandHealthTone = demandSupply.openTaskCount > 0 ? "text-emerald-300" : "text-amber-300";
const externalActorActivityMap = new Map<string, ExternalActorActivity>();
for (const event of recentEvents) {
if (!event.action.startsWith("EXTERNAL_")) {
@@ -293,6 +344,15 @@ async function getTrafficSummary(minutes: number) {
const value = typeof metadata?.user_agent === "string" ? metadata.user_agent : "unknown";
return value.trim().length > 0 ? value.trim() : "unknown";
})();
const taskId = (() => {
const value =
typeof event.entityId === "string"
? event.entityId
: typeof metadata?.task_id === "string"
? metadata.task_id
: "-";
return value;
})();
externalActorActivityMap.set(actorId, {
actorId,
@@ -301,16 +361,54 @@ async function getTrafficSummary(minutes: number) {
latestSurface: String(event.surface || "unknown"),
latestSourceIp: normalizedIp,
latestUserAgent: normalizedUa,
latestTaskId: taskId,
latestResponseStatus:
typeof event.response_status === "number" ? event.response_status : null,
latestResponseSummary: typeof event.response_summary === "string"
? event.response_summary
: "unknown",
latestReason: typeof event.reason === "string" ? event.reason : "unknown",
latestPayloadSummary:
typeof event.payload_summary === "string" ? event.payload_summary : "unknown",
latestRequestId: typeof event.request_id === "string" ? event.request_id : "n/a",
latestCreatedAt: event.createdAt.getTime(),
});
} else {
existing.events += 1;
if (event.createdAt.getTime() > existing.latestCreatedAt) {
const metadata = asRecordJson(event.metadata);
const taskId = (() => {
const value =
typeof event.entityId === "string"
? event.entityId
: typeof metadata?.task_id === "string"
? metadata.task_id
: "-";
return value;
})();
const normalizedIp = (() => {
const value = typeof metadata?.source_ip === "string" ? metadata.source_ip : "unknown";
return value.trim().length > 0 ? value.trim() : "unknown";
})();
const normalizedUa = (() => {
const value = typeof metadata?.user_agent === "string" ? metadata.user_agent : "unknown";
return value.trim().length > 0 ? value.trim() : "unknown";
})();
existing.latestAction = event.action;
existing.latestSurface = String(event.surface || "unknown");
existing.latestSourceIp = normalizedIp;
existing.latestUserAgent = normalizedUa;
existing.latestTaskId = taskId;
existing.latestResponseStatus =
typeof event.response_status === "number" ? event.response_status : null;
existing.latestResponseSummary =
typeof event.response_summary === "string" ? event.response_summary : "unknown";
existing.latestReason = typeof event.reason === "string" ? event.reason : "unknown";
existing.latestPayloadSummary =
typeof event.payload_summary === "string" ? event.payload_summary : "unknown";
existing.latestRequestId = typeof event.request_id === "string" ? event.request_id : "n/a";
existing.latestCreatedAt = event.createdAt.getTime();
}
}
}
@@ -342,6 +440,7 @@ async function getTrafficSummary(minutes: number) {
conversionRates,
externalActorActivities,
externalErrors,
demandSupply,
};
}
@@ -418,8 +517,11 @@ export default async function TrafficDashboard({
}
const summary = await getTrafficSummary(minutes);
const { conversionSummary, conversionRates } = summary;
const { conversionSummary, conversionRates, demandSupply } = summary;
const conversionHints = buildConversionTips(conversionRates, conversionSummary);
const hasClaimStall = summary.externalEventTypes.some((event) => event.action === "EXTERNAL_FUNNEL_CLAIM_STALL");
const demandHealthLabel = demandSupply.openTaskCount > 0 ? "有可接需求" : "無可接需求";
const demandHealthTone = demandSupply.openTaskCount > 0 ? "text-emerald-300" : "text-amber-300";
return (
<div className="min-h-screen bg-gray-950 text-gray-100 p-8 font-sans">
@@ -435,7 +537,7 @@ export default async function TrafficDashboard({
{summary.periodMinutes} `EXTERNAL_*`
</div>
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2">{summary.totalEvents}</div>
@@ -456,6 +558,35 @@ export default async function TrafficDashboard({
<div className="text-gray-400 text-sm"></div>
<div className="text-3xl font-bold mt-2 text-amber-300">{conversionSummary.submit_events}</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-4">
<div className="text-gray-400 text-sm"></div>
<div className={`text-3xl font-bold mt-2 ${demandHealthTone}`}>{demandSupply.openTaskCount}</div>
<div className="text-xs text-gray-400 mt-2">{demandHealthLabel}</div>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4"></h2>
<div className="space-y-2 text-sm">
<div className="flex justify-between"><span> OPEN </span><span className="text-emerald-300">{demandSupply.openTaskCount}</span></div>
<div className="flex justify-between"><span> {summary.periodMinutes} </span><span className="text-emerald-300">{demandSupply.newOpenTasksInWindow}</span></div>
<div className="flex justify-between"><span></span><span className="text-emerald-300">{demandSupply.avgScopeClarity === null ? "n/a" : `${Number(demandSupply.avgScopeClarity).toFixed(2)}`}</span></div>
</div>
<div className="mt-4 text-xs text-gray-400">
Task.status = OPEN title GitHub Issue: 開頭
</div>
</div>
<div className="bg-gray-900 border border-gray-800 rounded-2xl p-6">
<h2 className="text-xl font-semibold mb-4"></h2>
<ul className="text-sm text-gray-200 space-y-2 list-disc list-inside">
<li>1) <span className="text-emerald-300">{demandSupply.openTaskCount}</span> step</li>
<li>2) <span className="font-mono text-xs">/api/open-tasks</span> 3 30~60 </li>
<li>3) GitHub Issue:</li>
<li>4) rewardrequired_stack developer_wallet </li>
</ul>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
@@ -520,6 +651,16 @@ export default async function TrafficDashboard({
<li> PASS JUDGE_COMPLETE + overall_result=PASS CAPTURE</li>
<li>PASS</li>
</ol>
{hasClaimStall && demandSupply.openTaskCount > 0 ? (
<div className="mt-4 p-3 border border-amber-500/40 rounded-lg text-sm text-amber-200">
EXTERNAL_FUNNEL_CLAIM_STALL
<ul className="list-disc list-inside mt-2 space-y-1">
<li> OPEN `scope_clarity_score` reward </li>
<li> 1 `claim_task` 403 API Key/</li>
<li> 200 調</li>
</ul>
</div>
) : null}
<div className="mt-4 text-sm text-gray-400"> 30 </div>
</div>
</div>
@@ -566,15 +707,17 @@ export default async function TrafficDashboard({
<th className="text-left py-2">Actor</th>
<th className="text-left py-2"></th>
<th className="text-left py-2"></th>
<th className="text-left py-2"></th>
<th className="text-left py-2"> IP</th>
<th className="text-left py-2">User-Agent</th>
<th className="text-left py-2">Request-Id</th>
<th className="text-left py-2"></th>
</tr>
</thead>
<tbody>
{summary.externalActorActivities.length === 0 ? (
<tr>
<td colSpan={6} className="text-gray-500 py-3">
<td colSpan={8} className="text-gray-500 py-3">
AGENT AGENT
</td>
</tr>
@@ -587,11 +730,14 @@ export default async function TrafficDashboard({
<div className="text-gray-200">{actor.latestAction}</div>
<div className="text-xs text-gray-500">{actor.latestSurface}</div>
</td>
<td className="py-2 text-gray-300">{actor.latestTaskId}</td>
<td className="py-2 text-gray-300">{actor.latestSourceIp}</td>
<td className="py-2 text-gray-300">{actor.latestUserAgent}</td>
<td className="py-2 text-gray-300">{actor.latestRequestId}</td>
<td className="py-2">
<div className="text-gray-200">{actor.latestResponseStatus ?? "n/a"}</div>
<div className="text-xs text-gray-500">{actor.latestResponseSummary}</div>
<div className="text-xs text-gray-600">{actor.latestPayloadSummary}</div>
</td>
</tr>
))

View File

@@ -1,9 +1,87 @@
import Redis from "ioredis";
const globalForRedis = global as unknown as { redis: Redis };
type RedisLike = {
set(...args: any[]): Promise<string | null | undefined>;
};
export const redis =
const globalForRedis = global as unknown as { redis?: RedisLike };
const configuredRedisUrl = process.env.REDIS_URL?.trim();
class InMemoryRedisDedupe implements RedisLike {
private entries = new Map<string, number>();
private cleanup(now: number) {
for (const [key, expiryAt] of this.entries) {
if (expiryAt <= now) {
this.entries.delete(key);
}
}
}
async set(...args: any[]): Promise<string | null | undefined> {
const key = args[0] as string;
const mode = args[2] as string | undefined;
const ttlSeconds = typeof args[3] === "number" ? args[3] : undefined;
const condition = args[4] as string | undefined;
const now = Date.now();
this.cleanup(now);
if (condition === "NX" && this.entries.has(key)) {
return null;
}
if (mode && mode !== "EX") {
return null;
}
const ttl = ttlSeconds && ttlSeconds > 0 ? ttlSeconds * 1000 : 10 * 60 * 1000;
this.entries.set(key, now + ttl);
return "OK";
}
}
export const redis: RedisLike =
globalForRedis.redis ||
new Redis(process.env.REDIS_URL || "redis://localhost:6379");
(() => {
if (!configuredRedisUrl) {
if (process.env.NODE_ENV !== "test") {
console.info("[traffic-monitor] REDIS_URL is not set, fallback to in-memory dedupe." );
}
return new InMemoryRedisDedupe();
}
if (process.env.NODE_ENV !== "production") globalForRedis.redis = redis;
const redisClient = new Redis(configuredRedisUrl, {
maxRetriesPerRequest: 2,
enableOfflineQueue: true,
retryStrategy: (times) => {
if (times > 8) {
return 2000;
}
return Math.min(times * 200, 1000);
},
reconnectOnError: (error) => {
const message = error.message || "";
if (message.includes("READONLY")) return true;
if (message.includes("ECONNREFUSED")) return true;
if (message.includes("ENETUNREACH")) return true;
return false;
},
});
return redisClient;
})();
if (!globalForRedis.redis && configuredRedisUrl) {
let lastWarnAt = 0;
(redis as Redis).on("error", (error: Error) => {
const now = Date.now();
if (now - lastWarnAt < 60_000) return;
lastWarnAt = now;
console.warn("[Redis error]", error.message || String(error));
});
}
if (process.env.NODE_ENV !== "production") {
globalForRedis.redis = redis;
}

View File

@@ -1,5 +1,7 @@
import { prisma } from "./prisma";
import { Prisma } from "../../prisma/generated/client";
import dns from "node:dns";
import { request as httpsRequest } from "node:https";
const TELEGRAM_BOT_ID_FROM_TOKEN = (() => {
if (!process.env.TELEGRAM_BOT_TOKEN) return undefined;
@@ -46,6 +48,10 @@ const TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS = Math.max(
100,
Number.parseInt(process.env.TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS?.trim() || "400", 10) || 400
);
const TELEGRAM_IP_FAMILY = Number.parseInt(
process.env.TELEGRAM_IP_FAMILY?.trim() || "4",
10
) as 4 | 6;
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -71,18 +77,18 @@ async function resolveTelegramFallbackChatId(): Promise<string | undefined> {
}
try {
const response = await fetch(`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates?limit=20`, {
method: "GET",
signal: AbortSignal.timeout(TELEGRAM_NOTIFY_TIMEOUT_MS),
});
const response = await requestTelegram(
`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates?limit=20`,
"GET"
);
if (!response.ok) {
if (response.status < 200 || response.status >= 300) {
return undefined;
}
const data = (await response.json()) as {
const data = (JSON.parse(response.text) as {
result?: Array<Record<string, unknown>>;
};
}) || {};
const updates = Array.isArray(data.result) ? data.result : [];
for (let index = updates.length - 1; index >= 0; index -= 1) {
@@ -144,24 +150,26 @@ function buildTelegramMessage(event: TrafficAlertEvent) {
}
async function sendViaHttps(url: string, body: Record<string, unknown>) {
return new Promise<void>((resolve, reject) => {
return new Promise<{ messageId?: string }>((resolve, reject) => {
try {
const payload = JSON.stringify(body);
let responseBody = "";
fetch(url, {
method: "POST",
headers: {
"content-type": "application/json",
"content-length": String(Buffer.byteLength(payload)),
},
body: payload,
signal: AbortSignal.timeout(TELEGRAM_NOTIFY_TIMEOUT_MS),
})
requestTelegram(url, "POST", payload)
.then(async (response) => {
responseBody = await response.text();
const responseBody = response.text;
if (response.status >= 200 && response.status < 300) {
resolve();
let messageId: string | undefined;
try {
const parsed = JSON.parse(responseBody) as { result?: { message_id?: number } };
const parsedMessageId = parsed?.result?.message_id;
if (typeof parsedMessageId === "number") {
messageId = String(parsedMessageId);
}
} catch {
// ignore parse errors for non-JSON success responses
}
resolve({ messageId });
return;
}
@@ -180,6 +188,72 @@ async function sendViaHttps(url: string, body: Record<string, unknown>) {
});
}
type TelegramRequestResult = {
status: number;
text: string;
};
function requestTelegram(
url: string,
method: "GET" | "POST",
payload?: string
): Promise<TelegramRequestResult> {
return new Promise<TelegramRequestResult>((resolve, reject) => {
try {
const requestUrl = new URL(url);
const request = httpsRequest(
{
protocol: requestUrl.protocol,
hostname: requestUrl.hostname,
port: requestUrl.port,
path: `${requestUrl.pathname}${requestUrl.search}`,
method,
headers: {
...(payload
? {
"content-type": "application/json",
"content-length": String(Buffer.byteLength(payload)),
}
: {}),
},
timeout: TELEGRAM_NOTIFY_TIMEOUT_MS,
family: TELEGRAM_IP_FAMILY,
lookup: (hostname, options, callback) => {
dns.lookup(hostname, { ...options, family: TELEGRAM_IP_FAMILY }, callback);
},
},
(response) => {
let responseText = "";
response.setEncoding("utf8");
response.on("data", (chunk) => {
responseText += chunk;
});
response.on("end", () => {
resolve({
status: response.statusCode || 0,
text: responseText,
});
});
}
);
request.on("timeout", () => {
request.destroy(new Error(`Telegram API timeout ${TELEGRAM_NOTIFY_TIMEOUT_MS}ms`));
});
request.on("error", (error) => {
reject(error);
});
if (payload) {
request.write(payload);
}
request.end();
} catch (error) {
reject(error);
}
});
}
function resolveEntityFromTrafficEvent(event: TrafficAlertEvent) {
if (event.taskId) {
return { entityType: "TASK", entityId: event.taskId };
@@ -291,16 +365,31 @@ export async function sendTrafficAlert(event: TrafficAlertEvent): Promise<void>
? JSON.parse(typeof target.init.body === "string" ? target.init.body : "{}")
: {};
let attempt = 0;
while (true) {
for (let attempt = 1; ; attempt += 1) {
try {
await sendViaHttps(target.url, payload);
const result = await sendViaHttps(target.url, payload);
console.log(
"[Traffic alert notify ok] telegram",
`action=${event.action}`,
`surface=${event.surface}`,
`actor=${event.actorType}/${event.actorId}`,
`request_id=${typeof event.metadata?.request_id === "string" ? event.metadata.request_id : "n/a"}`,
`attempt=${attempt}`,
`message_id=${result.messageId || "n/a"}`
);
return;
} catch (error) {
attempt += 1;
if (attempt >= TELEGRAM_NOTIFY_MAX_ATTEMPTS) {
console.warn(
`[Traffic alert] telegram notify failed: ${event.action} attempt=${attempt}`,
error
);
throw error;
}
console.warn(
`[Traffic alert] telegram notify retrying action=${event.action} attempt=${attempt}`,
error
);
await sleep(TELEGRAM_NOTIFY_RETRY_BASE_DELAY_MS * attempt);
}
}

View File

@@ -1,6 +1,20 @@
version: '3.8'
services:
redis:
image: redis:7-alpine
container_name: agent_bounty_redis
restart: always
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
networks:
- agent-bounty-network
db:
image: postgres:14-alpine
container_name: agent_bounty_db
@@ -10,7 +24,7 @@ services:
POSTGRES_PASSWORD: agent_password_secure
POSTGRES_DB: agent_bounty
ports:
- "5432:5432"
- "5433:5432"
volumes:
- agent_bounty_pgdata:/var/lib/postgresql/data
networks:
@@ -28,11 +42,12 @@ services:
container_name: agent_bounty_web
restart: always
ports:
- "3000:3000"
- "3001:3000"
environment:
# Use the docker internal network to connect to postgres
- DATABASE_URL=postgresql://agent:agent_password_secure@db:5432/agent_bounty?schema=public
- NODE_ENV=production
- REDIS_URL=redis://redis:6379
- API_KEY=${API_KEY:-super-secret-mcp-key}
- E2B_API_KEY=${E2B_API_KEY:-""}
- AUTO_WHITELIST_EXTERNAL_AGENTS=${AUTO_WHITELIST_EXTERNAL_AGENTS:-true}
@@ -43,6 +58,8 @@ services:
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
# We use a command override to run database push before starting next.js
command: >
sh -c "npx prisma@6.4.1 db push --schema=apps/web/prisma/schema.prisma --skip-generate && node apps/web/server.js"

View File

@@ -1,6 +1,10 @@
#!/usr/bin/expect -f
set timeout -1
spawn ssh -J wooo@192.168.0.110 wooo@192.168.0.188 "cat /home/wooo/deployments/agent-bounty-protocol/.env"
set jump_host "wooo@192.168.0.110"
set target_host "ollama@192.168.0.188"
set repo_dir "/home/ollama/vibework-git"
spawn ssh -J $jump_host $target_host "cat $repo_dir/.env"
expect {
"*assword:*" {
send "0936223270\r"