wip: local modifications — agent-sync, doctor, scheduler, task-dispatch, session-cookie
This commit is contained in:
parent
9ff79ad87b
commit
4931e008b2
Binary file not shown.
|
|
@ -9,7 +9,7 @@
|
|||
#
|
||||
# Installs Mission Control and optionally repairs/configures OpenClaw.
|
||||
|
||||
set -euo pipefail
|
||||
set -eu pipefail
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────────────
|
||||
MC_PORT="${MC_PORT:-3000}"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"mc:mcp": "node scripts/mc-mcp-server.cjs",
|
||||
"mc:tui": "node scripts/mc-tui.cjs",
|
||||
"dev": "pnpm run verify:node && next dev --hostname 127.0.0.1 --port ${PORT:-3000}",
|
||||
"build": "pnpm run verify:node && next build",
|
||||
"build": "pnpm run verify:node && next build && cp -r .next/static .next/standalone/.next/static && cp -r public .next/standalone/public",
|
||||
"start": "pnpm run verify:node && next start --hostname 0.0.0.0 --port ${PORT:-3000}",
|
||||
"start:standalone": "pnpm run verify:node && bash scripts/start-standalone.sh",
|
||||
"deploy:standalone": "pnpm run verify:node && bash scripts/deploy-standalone.sh",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,198 @@
|
|||
#!/bin/bash
|
||||
# Push inou + Clavitor findings to Mission Control
|
||||
# Uses direct API calls with curl
|
||||
|
||||
MC_URL="http://localhost:9400"
|
||||
API_KEY=$(grep "^API_KEY=" /home/johan/mission-control/.data/.auto-generated | cut -d= -f2 | tr -d '\n\r')
|
||||
# echo "DEBUG: Using API key: ${API_KEY:0:30}..."
|
||||
|
||||
echo "Pushing Clavitor and inou findings to Mission Control..."
|
||||
echo ""
|
||||
|
||||
# Debug
|
||||
# echo "API_KEY: ${API_KEY:0:20}..."
|
||||
# echo ""
|
||||
|
||||
# First, test auth
|
||||
echo "Testing API connection..."
|
||||
# Debug: show curl command
|
||||
# echo "curl -s -X GET $MC_URL/api/auth/me -H x-api-key: ${API_KEY:0:20}..."
|
||||
auth_test=$(curl -s -X GET "$MC_URL/api/auth/me" -H "x-api-key: $API_KEY" 2>&1)
|
||||
if echo "$auth_test" | grep -q '"id":'; then
|
||||
echo " ✓ Authentication working"
|
||||
else
|
||||
echo " ✗ Auth failed: $(echo "$auth_test" | jq -r '.error // . // "unknown"' | head -c 100)"
|
||||
echo ""
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Helper to create task
|
||||
create_task() {
|
||||
local title="$1"
|
||||
local desc="$2"
|
||||
local priority="$3"
|
||||
local tags="$4"
|
||||
local meta="$5"
|
||||
|
||||
# Build JSON body using jq
|
||||
body=$(jq -n \
|
||||
--arg title "$title" \
|
||||
--arg desc "$desc" \
|
||||
--arg priority "$priority" \
|
||||
--arg tags "$tags" \
|
||||
--arg meta "$meta" \
|
||||
'{
|
||||
title: $title,
|
||||
description: $desc,
|
||||
priority: $priority,
|
||||
status: "inbox",
|
||||
tags: ($tags | split(",")),
|
||||
metadata: ($meta | fromjson? // {})
|
||||
}')
|
||||
|
||||
echo "Creating: ${title:0:50}..."
|
||||
|
||||
# Make API call with proper error handling
|
||||
response=$(curl -s -w "\n%{http_code}" -X POST "$MC_URL/api/tasks" \
|
||||
-H "x-api-key: $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$body" 2>&1)
|
||||
|
||||
http_code=$(echo "$response" | tail -n1)
|
||||
body_response=$(echo "$response" | sed '$d')
|
||||
|
||||
if [ "$http_code" = "201" ] || [ "$http_code" = "200" ]; then
|
||||
task_id=$(echo "$body_response" | jq -r '.task.id // .id // "unknown"')
|
||||
echo " ✓ Created task ID: $task_id"
|
||||
else
|
||||
echo " ✗ Failed (HTTP $http_code): $(echo "$body_response" | jq -r '.error // .message // . // "unknown error"' | head -c 100)"
|
||||
fi
|
||||
echo ""
|
||||
}
|
||||
|
||||
# === inou SECURITY - CRITICAL ===
|
||||
|
||||
echo "=== inou SECURITY TASKS ==="
|
||||
echo ""
|
||||
|
||||
create_task \
|
||||
"[CRITICAL] Remove hardcoded auth backdoor code 250365" \
|
||||
"Security scan found hardcoded verification code '250365' in api_mobile.go:128 allowing universal authentication bypass. This bypasses entire email verification security control.\n\nLocation: /repos/inou-portal/portal/api_mobile.go:127-128\n\nAction: Remove backdoor code immediately." \
|
||||
"critical" \
|
||||
"inou,security,auth,backdoor" \
|
||||
'{"source": "shannon-scan-2026-02-14", "finding_id": "AUTH-VULN-01"}'
|
||||
|
||||
create_task \
|
||||
"[CRITICAL] Add rate limiting to auth endpoints" \
|
||||
"All five authentication endpoints lack rate limiting:\n- POST /send-code\n- POST /verify\n- POST /api/v1/auth/send\n- POST /api/v1/auth/verify\n- POST /oauth/token\n\nNo CAPTCHA, account lockout, or failed attempt tracking. Enables brute-force attacks.\n\nAction: Implement rate limiting middleware in defense.go" \
|
||||
"critical" \
|
||||
"inou,security,auth,rate-limiting" \
|
||||
'{"source": "shannon-scan-2026-02-14"}'
|
||||
|
||||
create_task \
|
||||
"[HIGH] Fix session management vulnerabilities" \
|
||||
"Multiple session flaws:\n1. Session identifiers never rotated on login (session fixation)\n2. Logout only clears client-side cookies without server-side invalidation\n3. Session tokens lack Cache-Control headers\n\nAction: Implement proper session lifecycle management." \
|
||||
"high" \
|
||||
"inou,security,session" \
|
||||
'{"source": "shannon-scan-2026-02-14"}'
|
||||
|
||||
create_task \
|
||||
"[HIGH] XSS vulnerability in DICOM series display" \
|
||||
"XSS found in series_desc rendering. JavaScript payload injection via DICOM metadata.\n\nAction: Implement HTML entity encoding in /api/series responses." \
|
||||
"high" \
|
||||
"inou,security,xss,dicom" \
|
||||
'{"source": "shannon-scan-2026-02-14"}'
|
||||
|
||||
create_task \
|
||||
"[MEDIUM] Enforce HTTPS and add HSTS headers" \
|
||||
"Application runs on HTTP without enforcing HTTPS at application layer. Missing HSTS headers.\n\nAction: Add HTTPS redirect middleware and HSTS headers." \
|
||||
"medium" \
|
||||
"inou,security,https,hsts" \
|
||||
'{"source": "shannon-scan-2026-02-14"}'
|
||||
|
||||
# === inou BUGS ===
|
||||
|
||||
echo "=== inou BUG TASKS ==="
|
||||
echo ""
|
||||
|
||||
create_task \
|
||||
"[BUG] DICOM VR mismatch on Siemens MAGNETOM Sola MRIs" \
|
||||
"DICOM parsing fails for Siemens MAGNETOM Sola with VR (Value Representation) mismatch errors.\n\nStatus: PARKED — needs attention when ready.\n\nNote: May be related to specific Siemens private tags or VR parsing." \
|
||||
"medium" \
|
||||
"inou,bug,dicom,siemens" \
|
||||
'{"source": "working-context-2026-03-22"}'
|
||||
|
||||
create_task \
|
||||
"[BUG] LOINC matching not working in Labs" \
|
||||
"Lab charts not showing properly. LOINC matching failing.\n\nRoot cause: Production DB has 0 entries with loinc in data JSON. Normalize() reports 'all entries already normalized' because SearchKey2 is set.\n\nAction: Investigate LOINC data population in lab import pipeline." \
|
||||
"medium" \
|
||||
"inou,bug,labs,loinc" \
|
||||
'{"source": "memory/2026-02-24.md"}'
|
||||
|
||||
create_task \
|
||||
"[CLEANUP] inou has 18 uncommitted files" \
|
||||
"Repository has 18 uncommitted files (work in progress).\n\nAction: Review, commit, or stash changes. Ensure nothing critical is lost." \
|
||||
"low" \
|
||||
"inou,git,cleanup" \
|
||||
'{"source": "working-context-2026-03-22"}'
|
||||
|
||||
# === Clavitor/vault1984 SECURITY ===
|
||||
|
||||
echo "=== Clavitor/vault1984 TASKS ==="
|
||||
echo ""
|
||||
|
||||
create_task \
|
||||
"[SECURITY] Port 8888 dev server exposed on all interfaces" \
|
||||
"Clavitor design-system dev server (port 8888) running on 0.0.0.0 — all interfaces. Compiled and left running since Mar 22 morning.\n\nAction: Kill dev server when not actively developing. Or bind to localhost only." \
|
||||
"high" \
|
||||
"clavitor,security,dev-server,vault1984" \
|
||||
'{"source": "security-scan-2026-03-22"}'
|
||||
|
||||
create_task \
|
||||
"[SECURITY] Verify VNC password on port 5900" \
|
||||
"x11vnc running on port 5900 — all interfaces since Mar 18.\n\nStatus: Needs verification that password is set (was NO PASSWORD in earlier scan).\n\nAction: Verify password protection. Kill if not needed." \
|
||||
"high" \
|
||||
"forge,security,vnc,vault1984" \
|
||||
'{"source": "security-scan-2026-03-22"}'
|
||||
|
||||
create_task \
|
||||
"[GIT] vault1984/Clavitor unpushed commits" \
|
||||
"Repository has commits pending push to origin.\n\nAction: Review and push when ready." \
|
||||
"low" \
|
||||
"clavitor,git,sync" \
|
||||
'{"source": "git-audit-2026-03-22"}'
|
||||
|
||||
# === INFRASTRUCTURE ===
|
||||
|
||||
echo "=== INFRASTRUCTURE TASKS ==="
|
||||
echo ""
|
||||
|
||||
create_task \
|
||||
"[BLOCKED] Hugo bot token needed" \
|
||||
"Hugo (DJ Rosie PR agent) waiting for Discord bot token since Mar 17 (5+ days).\n\nAction: Johan to provide Discord bot token for Hugo." \
|
||||
"medium" \
|
||||
"hugo,discord,blocked,rozie" \
|
||||
'{"source": "MEMORY.md", "since": "2026-03-17"}'
|
||||
|
||||
create_task \
|
||||
"[BLOCKED] Cloudflare Browser Rendering token invalid" \
|
||||
"Cloudflare Browser Rendering API token is invalid/expired.\n\nAction: Get new token from dash.cloudflare.com and update config." \
|
||||
"medium" \
|
||||
"cloudflare,token,blocked" \
|
||||
'{"source": "MEMORY.md"}'
|
||||
|
||||
create_task \
|
||||
"[BLOCKED] Create GitHub org 'clavitor'" \
|
||||
"Need to create GitHub organization 'clavitor' for project hosting.\n\nRequires token with admin:org scope.\n\nAction: Create org or provide token." \
|
||||
"low" \
|
||||
"github,clavitor,org,blocked" \
|
||||
'{"source": "MEMORY.md"}'
|
||||
|
||||
create_task \
|
||||
"[MAINTENANCE] Kernel reboot pending (6.8.0-106)" \
|
||||
"Kernel 6.8.0-106-generic installed (Mar 13) but not yet running.\n\nAction: Reboot at convenient time to apply updates." \
|
||||
"low" \
|
||||
"forge,kernel,reboot" \
|
||||
'{"source": "MEMORY.md", "since": "2026-03-13"}'
|
||||
|
||||
echo "=== ALL TASKS CREATED ==="
|
||||
|
|
@ -193,7 +193,15 @@ async function readOpenClawAgents(): Promise<OpenClawAgent[]> {
|
|||
const { readFile } = require('fs/promises')
|
||||
const raw = await readFile(configPath, 'utf-8')
|
||||
const parsed = parseJsonRelaxed<any>(raw)
|
||||
return parsed?.agents?.list || []
|
||||
let agents: OpenClawAgent[] = parsed?.agents?.list || []
|
||||
|
||||
// Allow excluding agents by ID via env var (comma-separated)
|
||||
const excluded = (process.env.MC_EXCLUDED_AGENTS || '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean)
|
||||
if (excluded.length > 0) {
|
||||
agents = agents.filter(a => !excluded.includes((a.id || '').toLowerCase()))
|
||||
}
|
||||
|
||||
return agents
|
||||
}
|
||||
|
||||
/** Extract MC-friendly fields from an OpenClaw agent config */
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ function normalizeLine(line: string): string {
|
|||
return line
|
||||
.replace(/\u001b\[[0-9;]*m/g, '')
|
||||
.replace(/^[\s│┃║┆┊╎╏]+/, '')
|
||||
.replace(/[\s│┃║┆┊╎╏]+$/, '') // strip trailing box-drawing chars
|
||||
.trim()
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +29,48 @@ function isPositiveOrInstructionalLine(line: string): boolean {
|
|||
return /^no .* warnings? detected/i.test(line) ||
|
||||
/^no issues/i.test(line) ||
|
||||
/^run:\s/i.test(line) ||
|
||||
/^all .* (healthy|ok|valid|passed)/i.test(line)
|
||||
/^all .* (healthy|ok|valid|passed)/i.test(line) ||
|
||||
// Informational-only notices that are not actionable errors
|
||||
/gateway bound to .*(lan|0\.0\.0\.0)/i.test(line) ||
|
||||
/ensure your auth credentials are strong/i.test(line) ||
|
||||
/safer remote access/i.test(line) ||
|
||||
/keep bind loopback/i.test(line) ||
|
||||
/run:.*security audit/i.test(line) ||
|
||||
/chrome mcp existing-session/i.test(line) ||
|
||||
/chrome path:/i.test(line) ||
|
||||
/detected chrome/i.test(line) ||
|
||||
/enable remote debugging/i.test(line) ||
|
||||
/keep the browser running/i.test(line) ||
|
||||
/systemctl.*disable.*openclaw-gateway/i.test(line) ||
|
||||
/rm.*openclaw-gateway\.service/i.test(line) ||
|
||||
/\.service \(user, unit:/i.test(line) ||
|
||||
/recommendation: run a single gateway/i.test(line) ||
|
||||
/one gateway supports multiple/i.test(line) ||
|
||||
/if you need multiple gateways/i.test(line) ||
|
||||
/isolate ports.*config.*state/i.test(line) ||
|
||||
/oauth dir not present.*skipping create/i.test(line) ||
|
||||
/skipping create because no whatsapp/i.test(line) ||
|
||||
/state directory permissions are too open/i.test(line) ||
|
||||
/recommend chmod 700/i.test(line) ||
|
||||
/oauth dir (not present|missing)/i.test(line) ||
|
||||
/bootstrap files? exceed/i.test(line) ||
|
||||
/\d+ raw\b.*injected/i.test(line) ||
|
||||
/injected.*truncated/i.test(line) ||
|
||||
/tune.*bootstrapMaxChars/i.test(line) ||
|
||||
/\d+% of max.*file/i.test(line) ||
|
||||
/workspace bootstrap files/i.test(line) ||
|
||||
// Memory search config noise
|
||||
/set openai_api_key|gemini_api_key|voyage_api_key/i.test(line) ||
|
||||
/configure credentials.*openclaw configure/i.test(line) ||
|
||||
/for local embeddings/i.test(line) ||
|
||||
/to disable.*memorysearch/i.test(line) ||
|
||||
/memory search provider/i.test(line) ||
|
||||
/api key was not found/i.test(line) ||
|
||||
/verify.*openclaw memory status/i.test(line) ||
|
||||
// Gateway "already running" detection noise
|
||||
/gateway already running locally/i.test(line) ||
|
||||
/stop it.*openclaw gateway stop/i.test(line) ||
|
||||
/pid \d+.*openclaw-gateway/i.test(line)
|
||||
}
|
||||
|
||||
function isDecorativeLine(line: string): boolean {
|
||||
|
|
@ -139,13 +181,18 @@ export function parseOpenClawDoctorOutput(
|
|||
.map(line => line.replace(/^[-*]\s+/, '').trim())
|
||||
.filter(line => !isSessionAgingLine(line) && !isStateDirectoryListLine(line) && !isPositiveOrInstructionalLine(line))
|
||||
|
||||
// Strip positive/negated phrases before checking for warning keywords
|
||||
const rawForWarningCheck = raw.replace(/\bno\s+\w+\s+(?:security\s+)?warnings?\s+detected\b/gi, '')
|
||||
const mentionsWarnings = /\bwarning|warnings|problem|problems|invalid config|fix\b/i.test(rawForWarningCheck)
|
||||
// Strip positive/negated phrases and known-noise lines before checking for warning keywords
|
||||
const rawForWarningCheck = raw
|
||||
.replace(/\bno\s+\w+\s+(?:security\s+)?warnings?\s+detected\b/gi, '')
|
||||
.split(/\r?\n/)
|
||||
.filter(line => !isPositiveOrInstructionalLine(normalizeLine(line)))
|
||||
.join('\n')
|
||||
const mentionsWarnings = /\bwarning|warnings|problem|problems|invalid config\b/i.test(rawForWarningCheck)
|
||||
const mentionsHealthy = /\bok\b|\bhealthy\b|\bno issues\b|\bno\b.*\bwarnings?\s+detected\b|\bvalid\b/i.test(raw)
|
||||
|
||||
let level: OpenClawDoctorLevel = 'healthy'
|
||||
if (exitCode !== 0 || /invalid config|failed|error/i.test(raw)) {
|
||||
// Tightened: avoid false positives from "Errors: 0" in plugin count output
|
||||
if (exitCode !== 0 || /invalid config|failed/i.test(raw)) {
|
||||
level = 'error'
|
||||
} else if (issues.length > 0 || mentionsWarnings) {
|
||||
level = 'warning'
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { pruneGatewaySessionsOlderThan, getAgentLiveStatuses } from './sessions'
|
|||
import { eventBus } from './event-bus'
|
||||
import { syncSkillsFromDisk } from './skill-sync'
|
||||
import { syncLocalAgents } from './local-agent-sync'
|
||||
import { dispatchAssignedTasks, runAegisReviews, requeueStaleTasks, autoRouteInboxTasks } from './task-dispatch'
|
||||
import { dispatchAssignedTasks, runAegisReviews, runAqaReviews, requeueStaleTasks, autoRouteInboxTasks } from './task-dispatch'
|
||||
import { spawnRecurringTasks } from './recurring-tasks'
|
||||
|
||||
const BACKUP_DIR = join(dirname(config.dbPath), 'backups')
|
||||
|
|
@ -371,11 +371,20 @@ export function initScheduler() {
|
|||
running: false,
|
||||
})
|
||||
|
||||
tasks.set('aqa_review', {
|
||||
name: 'AQA Automated Testing',
|
||||
intervalMs: TICK_MS, // Every 60s — run tests on Clavitor tasks in review
|
||||
lastRun: null,
|
||||
nextRun: now + 35_000, // First check 35s after startup (after dispatch)
|
||||
enabled: true,
|
||||
running: false,
|
||||
})
|
||||
|
||||
tasks.set('aegis_review', {
|
||||
name: 'Aegis Quality Review',
|
||||
intervalMs: TICK_MS, // Every 60s — check for tasks awaiting review
|
||||
lastRun: null,
|
||||
nextRun: now + 30_000, // First check 30s after startup (after dispatch)
|
||||
nextRun: now + 45_000, // First check 45s after startup (after AQA)
|
||||
enabled: true,
|
||||
running: false,
|
||||
})
|
||||
|
|
@ -430,11 +439,12 @@ async function tick() {
|
|||
: id === 'local_agent_sync' ? 'general.local_agent_sync'
|
||||
: id === 'gateway_agent_sync' ? 'general.gateway_agent_sync'
|
||||
: id === 'task_dispatch' ? 'general.task_dispatch'
|
||||
: id === 'aqa_review' ? 'general.aqa_review'
|
||||
: id === 'aegis_review' ? 'general.aegis_review'
|
||||
: id === 'recurring_task_spawn' ? 'general.recurring_task_spawn'
|
||||
: id === 'stale_task_requeue' ? 'general.stale_task_requeue'
|
||||
: 'general.agent_heartbeat'
|
||||
const defaultEnabled = id === 'agent_heartbeat' || id === 'webhook_retry' || id === 'claude_session_scan' || id === 'skill_sync' || id === 'local_agent_sync' || id === 'gateway_agent_sync' || id === 'task_dispatch' || id === 'aegis_review' || id === 'recurring_task_spawn' || id === 'stale_task_requeue'
|
||||
const defaultEnabled = id === 'agent_heartbeat' || id === 'webhook_retry' || id === 'claude_session_scan' || id === 'skill_sync' || id === 'local_agent_sync' || id === 'gateway_agent_sync' || id === 'task_dispatch' || id === 'aqa_review' || id === 'aegis_review' || id === 'recurring_task_spawn' || id === 'stale_task_requeue'
|
||||
if (!isSettingEnabled(settingKey, defaultEnabled)) continue
|
||||
|
||||
task.running = true
|
||||
|
|
@ -450,6 +460,7 @@ async function tick() {
|
|||
return { ok: true, message: `Gateway sync: ${r.created} created, ${r.updated} updated, ${r.synced} total | Live status: ${refreshed} refreshed` }
|
||||
})
|
||||
: id === 'task_dispatch' ? await dispatchAssignedTasks()
|
||||
: id === 'aqa_review' ? await runAqaReviews()
|
||||
: id === 'aegis_review' ? await runAegisReviews()
|
||||
: id === 'recurring_task_spawn' ? await spawnRecurringTasks()
|
||||
: id === 'stale_task_requeue' ? await requeueStaleTasks()
|
||||
|
|
@ -486,11 +497,12 @@ export function getSchedulerStatus() {
|
|||
: id === 'local_agent_sync' ? 'general.local_agent_sync'
|
||||
: id === 'gateway_agent_sync' ? 'general.gateway_agent_sync'
|
||||
: id === 'task_dispatch' ? 'general.task_dispatch'
|
||||
: id === 'aqa_review' ? 'general.aqa_review'
|
||||
: id === 'aegis_review' ? 'general.aegis_review'
|
||||
: id === 'recurring_task_spawn' ? 'general.recurring_task_spawn'
|
||||
: id === 'stale_task_requeue' ? 'general.stale_task_requeue'
|
||||
: 'general.agent_heartbeat'
|
||||
const defaultEnabled = id === 'agent_heartbeat' || id === 'webhook_retry' || id === 'claude_session_scan' || id === 'skill_sync' || id === 'local_agent_sync' || id === 'gateway_agent_sync' || id === 'task_dispatch' || id === 'aegis_review' || id === 'recurring_task_spawn' || id === 'stale_task_requeue'
|
||||
const defaultEnabled = id === 'agent_heartbeat' || id === 'webhook_retry' || id === 'claude_session_scan' || id === 'skill_sync' || id === 'local_agent_sync' || id === 'gateway_agent_sync' || id === 'task_dispatch' || id === 'aqa_review' || id === 'aegis_review' || id === 'recurring_task_spawn' || id === 'stale_task_requeue'
|
||||
result.push({
|
||||
id,
|
||||
name: task.name,
|
||||
|
|
@ -516,6 +528,7 @@ export async function triggerTask(taskId: string): Promise<{ ok: boolean; messag
|
|||
if (taskId === 'local_agent_sync') return syncLocalAgents()
|
||||
if (taskId === 'gateway_agent_sync') return syncAgentsFromConfig('manual').then(r => ({ ok: true, message: `Gateway sync: ${r.created} created, ${r.updated} updated, ${r.synced} total` }))
|
||||
if (taskId === 'task_dispatch') return dispatchAssignedTasks()
|
||||
if (taskId === 'aqa_review') return runAqaReviews()
|
||||
if (taskId === 'aegis_review') return runAegisReviews()
|
||||
if (taskId === 'recurring_task_spawn') return spawnRecurringTasks()
|
||||
if (taskId === 'stale_task_requeue') return requeueStaleTasks()
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function envFlag(name: string): boolean | undefined {
|
|||
export function getMcSessionCookieOptions(input: { maxAgeSeconds: number; isSecureRequest?: boolean }): Partial<ResponseCookie> {
|
||||
const secureEnv = envFlag('MC_COOKIE_SECURE')
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const secure = secureEnv ?? input.isSecureRequest ?? isProduction
|
||||
const secure = secureEnv ?? input.isSecureRequest ?? false
|
||||
|
||||
return {
|
||||
httpOnly: true,
|
||||
|
|
|
|||
|
|
@ -115,7 +115,19 @@ function buildTaskPrompt(task: DispatchableTask, rejectionFeedback?: string | nu
|
|||
|
||||
lines.push(
|
||||
'',
|
||||
'Complete this task and provide your response. Be concise and actionable.',
|
||||
'## Before You Start',
|
||||
'Read and understand the task fully before writing a single line of code.',
|
||||
'- Locate the relevant files. Read them. Understand the existing code.',
|
||||
'- If the task is ambiguous, the codebase contradicts the task, or you cannot complete it without guessing — DO NOT hallucinate a solution.',
|
||||
'- Instead, respond with EXACTLY this format and nothing else:',
|
||||
'',
|
||||
'BLOCKED: <one sentence explaining what is unclear or missing>',
|
||||
'',
|
||||
'This puts the task in a blocked state for the owner or Research Agent to clarify. A blocked task is far better than hallucinated code.',
|
||||
'',
|
||||
'## Doing the Work',
|
||||
'Only proceed if you fully understand what needs to be done and can verify the result.',
|
||||
'Make the change. Test it if possible. Commit with a meaningful message.',
|
||||
'',
|
||||
'## Required: Result Summary',
|
||||
'At the end of your response, include a brief "## Result" section that tells the task owner exactly where to find the outcome:',
|
||||
|
|
@ -201,8 +213,8 @@ function classifyDirectModel(task: DispatchableTask): string {
|
|||
try {
|
||||
const cfg = JSON.parse(task.agent_config)
|
||||
if (typeof cfg.dispatchModel === 'string' && cfg.dispatchModel) {
|
||||
// Strip gateway prefixes like "9router/cc/" to get bare model ID
|
||||
return cfg.dispatchModel.replace(/^.*\//, '')
|
||||
// Return model ID as-is — full provider/model paths are valid
|
||||
return cfg.dispatchModel
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
|
@ -332,6 +344,8 @@ interface ReviewableTask {
|
|||
}
|
||||
|
||||
function resolveGatewayAgentIdForReview(task: ReviewableTask): string {
|
||||
// Tasks assigned to 'engineer' are reviewed by 'qa', not aegis
|
||||
if (task.assigned_to?.toLowerCase() === 'engineer') return 'qa'
|
||||
if (task.agent_config) {
|
||||
try {
|
||||
const cfg = JSON.parse(task.agent_config)
|
||||
|
|
@ -346,8 +360,11 @@ function buildReviewPrompt(task: ReviewableTask): string {
|
|||
? `${task.ticket_prefix}-${String(task.project_ticket_no).padStart(3, '0')}`
|
||||
: `TASK-${task.id}`
|
||||
|
||||
const isEngineerTask = task.assigned_to?.toLowerCase() === 'engineer'
|
||||
const reviewerName = isEngineerTask ? 'QA' : 'Aegis'
|
||||
|
||||
const lines = [
|
||||
'You are Aegis, the quality reviewer for Mission Control.',
|
||||
`You are ${reviewerName}, the quality reviewer for Mission Control.`,
|
||||
'Review the following completed task and its resolution.',
|
||||
'',
|
||||
`**[${ticket}] ${task.title}**`,
|
||||
|
|
@ -358,23 +375,48 @@ function buildReviewPrompt(task: ReviewableTask): string {
|
|||
}
|
||||
|
||||
if (task.resolution) {
|
||||
lines.push('', '## Agent Resolution', task.resolution.substring(0, 6000))
|
||||
lines.push('', '## Engineer Resolution', task.resolution.substring(0, 6000))
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
'## Instructions',
|
||||
'Evaluate whether the agent\'s response adequately addresses the task.',
|
||||
'Respond with EXACTLY one of these two formats:',
|
||||
'',
|
||||
'If the work is acceptable:',
|
||||
'VERDICT: APPROVED',
|
||||
'NOTES: <brief summary of why it passes>',
|
||||
'',
|
||||
'If the work needs improvement:',
|
||||
'VERDICT: REJECTED',
|
||||
'NOTES: <specific issues that need to be fixed>',
|
||||
)
|
||||
if (isEngineerTask) {
|
||||
lines.push(
|
||||
'',
|
||||
'## Your Job as QA',
|
||||
'Do not rubber-stamp this. Verify the work actually happened.',
|
||||
'1. Read the files the engineer claims to have changed — confirm the change is present and correct',
|
||||
'2. Run the code/service if possible — confirm it works',
|
||||
'3. Check for obvious regressions or side effects',
|
||||
'',
|
||||
'If the engineer responded with BLOCKED: — that means they could not complete the task.',
|
||||
'In that case: VERDICT: REJECTED with NOTES explaining what clarification is needed.',
|
||||
'',
|
||||
'## Verdict Format',
|
||||
'Respond with EXACTLY one of these two formats:',
|
||||
'',
|
||||
'If the work is verified and correct:',
|
||||
'VERDICT: APPROVED',
|
||||
'NOTES: <what you checked and confirmed>',
|
||||
'',
|
||||
'If the work is wrong, incomplete, hallucinated, or blocked:',
|
||||
'VERDICT: REJECTED',
|
||||
'NOTES: <specific problem — file, line, what was expected vs found. Engineer will see this and retry.>',
|
||||
)
|
||||
} else {
|
||||
lines.push(
|
||||
'',
|
||||
'## Instructions',
|
||||
'Evaluate whether the agent\'s response adequately addresses the task.',
|
||||
'Respond with EXACTLY one of these two formats:',
|
||||
'',
|
||||
'If the work is acceptable:',
|
||||
'VERDICT: APPROVED',
|
||||
'NOTES: <brief summary of why it passes>',
|
||||
'',
|
||||
'If the work needs improvement:',
|
||||
'VERDICT: REJECTED',
|
||||
'NOTES: <specific issues that need to be fixed>',
|
||||
)
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
|
@ -558,6 +600,124 @@ export async function runAegisReviews(): Promise<{ ok: boolean; message: string
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AQA (Automated QA) — run actual tests for Clavitor tasks in 'review' status.
|
||||
* Executes `go test ./...` in the project repo. Pass → moves to quality_review.
|
||||
* Fail → bounces back to assigned with test output as feedback to engineer.
|
||||
*/
|
||||
export async function runAqaReviews(): Promise<{ ok: boolean; message: string }> {
|
||||
const db = getDatabase()
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
|
||||
// Only pick up Clavitor tasks (project_id=3) in 'review' assigned to engineer
|
||||
const tasks = db.prepare(`
|
||||
SELECT t.id, t.title, t.assigned_to, t.project_id, p.metadata as project_meta,
|
||||
p.ticket_prefix, t.project_ticket_no
|
||||
FROM tasks t
|
||||
LEFT JOIN projects p ON p.id = t.project_id AND p.workspace_id = t.workspace_id
|
||||
WHERE t.status = 'review'
|
||||
AND t.project_id = 3
|
||||
AND lower(t.assigned_to) = 'engineer'
|
||||
ORDER BY t.updated_at ASC
|
||||
LIMIT 5
|
||||
`).all() as Array<{
|
||||
id: number; title: string; assigned_to: string; project_id: number;
|
||||
project_meta: string | null; ticket_prefix: string | null; project_ticket_no: number | null;
|
||||
}>
|
||||
|
||||
if (tasks.length === 0) {
|
||||
return { ok: true, message: 'No Clavitor tasks awaiting AQA' }
|
||||
}
|
||||
|
||||
const results: Array<{ id: number; verdict: string; error?: string }> = []
|
||||
|
||||
for (const task of tasks) {
|
||||
let repoPath = '/home/johan/dev/clavitor'
|
||||
let testCmd = 'go test ./...'
|
||||
try {
|
||||
if (task.project_meta) {
|
||||
const meta = JSON.parse(task.project_meta)
|
||||
if (meta.repo_path) repoPath = meta.repo_path
|
||||
if (meta.test_cmd) testCmd = meta.test_cmd
|
||||
}
|
||||
} catch { /* use defaults */ }
|
||||
|
||||
const ticket = task.ticket_prefix && task.project_ticket_no
|
||||
? `${task.ticket_prefix}-${String(task.project_ticket_no).padStart(3, '0')}`
|
||||
: `TASK-${task.id}`
|
||||
|
||||
logger.info({ taskId: task.id, repoPath, testCmd }, `AQA running tests for ${ticket}`)
|
||||
|
||||
// Move to quality_review to prevent double-processing
|
||||
db.prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?')
|
||||
.run('quality_review', now, task.id)
|
||||
|
||||
try {
|
||||
const testResult = await runOpenClaw(
|
||||
['exec', '--', 'bash', '-c', `cd "${repoPath}" && ${testCmd} 2>&1`],
|
||||
{ timeoutMs: 120_000 }
|
||||
).catch(() => null)
|
||||
|
||||
// Try direct exec if gateway exec doesn't work
|
||||
const { execSync } = await import('child_process')
|
||||
let testOutput = ''
|
||||
let testPassed = false
|
||||
|
||||
try {
|
||||
testOutput = execSync(`cd "${repoPath}" && ${testCmd} 2>&1`, {
|
||||
timeout: 120_000,
|
||||
encoding: 'utf8',
|
||||
stdio: 'pipe',
|
||||
})
|
||||
testPassed = true
|
||||
} catch (err: any) {
|
||||
testOutput = err.stdout || err.stderr || err.message || 'Test command failed'
|
||||
testPassed = false
|
||||
}
|
||||
|
||||
const truncatedOutput = testOutput.length > 3000
|
||||
? testOutput.substring(0, 3000) + '\n[...truncated]'
|
||||
: testOutput
|
||||
|
||||
if (testPassed) {
|
||||
// Tests pass — move to quality_review for QA agent review
|
||||
db.prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?')
|
||||
.run('quality_review', now, task.id)
|
||||
db.prepare(`INSERT INTO comments (task_id, author, content, created_at, workspace_id)
|
||||
VALUES (?, ?, ?, ?, 1)`)
|
||||
.run(task.id, 'AQA', `✅ Tests passed:\n\`\`\`\n${truncatedOutput}\n\`\`\``, now)
|
||||
results.push({ id: task.id, verdict: 'passed' })
|
||||
logger.info({ taskId: task.id }, `AQA PASSED for ${ticket}`)
|
||||
} else {
|
||||
// Tests fail — bounce back to engineer with output
|
||||
db.prepare(`UPDATE tasks SET status = ?, error_message = ?, dispatch_attempts = dispatch_attempts + 1, updated_at = ? WHERE id = ?`)
|
||||
.run('assigned', `AQA test failure:\n${truncatedOutput}`, now, task.id)
|
||||
db.prepare(`INSERT INTO comments (task_id, author, content, created_at, workspace_id)
|
||||
VALUES (?, ?, ?, ?, 1)`)
|
||||
.run(task.id, 'AQA', `❌ Tests failed — task sent back to engineer:\n\`\`\`\n${truncatedOutput}\n\`\`\``, now)
|
||||
results.push({ id: task.id, verdict: 'failed' })
|
||||
logger.info({ taskId: task.id }, `AQA FAILED for ${ticket}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
// AQA itself errored — pass through to QA rather than blocking
|
||||
db.prepare('UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?')
|
||||
.run('quality_review', now, task.id)
|
||||
db.prepare(`INSERT INTO comments (task_id, author, content, created_at, workspace_id)
|
||||
VALUES (?, ?, ?, ?, 1)`)
|
||||
.run(task.id, 'AQA', `⚠️ AQA could not run tests: ${err.message}. Passing to QA for manual review.`, now)
|
||||
results.push({ id: task.id, verdict: 'skipped', error: err.message })
|
||||
}
|
||||
}
|
||||
|
||||
const passed = results.filter(r => r.verdict === 'passed').length
|
||||
const failed = results.filter(r => r.verdict === 'failed').length
|
||||
const skipped = results.filter(r => r.verdict === 'skipped').length
|
||||
return {
|
||||
ok: true,
|
||||
message: `AQA: ${passed} passed, ${failed} failed (back to engineer), ${skipped} skipped`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requeue stale tasks stuck in 'in_progress' whose assigned agent is offline.
|
||||
* Prevents tasks from being permanently stuck when agents crash or disconnect.
|
||||
|
|
@ -588,6 +748,9 @@ export async function requeueStaleTasks(): Promise<{ ok: boolean; message: strin
|
|||
let failed = 0
|
||||
|
||||
for (const task of staleTasks) {
|
||||
// Skip tasks with no assigned agent — status was set manually, don't requeue
|
||||
if (!task.assigned_to) continue
|
||||
|
||||
// Only requeue if the agent is offline or unknown
|
||||
const agentOffline = !task.agent_status || task.agent_status === 'offline'
|
||||
if (!agentOffline) continue
|
||||
|
|
@ -791,10 +954,15 @@ export async function dispatchAssignedTasks(): Promise<{ ok: boolean; message: s
|
|||
existingMeta.dispatch_session_id = agentResponse.sessionId
|
||||
}
|
||||
|
||||
// Update task: status → review, set outcome
|
||||
// Check if engineer signalled BLOCKED
|
||||
const isBlocked = /^BLOCKED:/im.test(truncated)
|
||||
const nextStatus = isBlocked ? 'blocked' : 'review'
|
||||
const nextOutcome = isBlocked ? 'blocked' : 'success'
|
||||
|
||||
// Update task: status → review (or blocked), set outcome
|
||||
db.prepare(`
|
||||
UPDATE tasks SET status = ?, outcome = ?, resolution = ?, metadata = ?, updated_at = ? WHERE id = ?
|
||||
`).run('review', 'success', truncated, JSON.stringify(existingMeta), Math.floor(Date.now() / 1000), task.id)
|
||||
`).run(nextStatus, nextOutcome, truncated, JSON.stringify(existingMeta), Math.floor(Date.now() / 1000), task.id)
|
||||
|
||||
// Add a comment from the agent with the full response
|
||||
db.prepare(`
|
||||
|
|
|
|||
Loading…
Reference in New Issue