fix: support gateway defaults for spawn and Docker Hub publish
- make /api/spawn compatible with gateway-managed default models - add regression coverage for gateway dashboard registration - publish official multi-arch images to Docker Hub when configured
This commit is contained in:
parent
fc4384bba9
commit
8517d5e896
|
|
@ -20,8 +20,9 @@ permissions:
|
|||
id-token: write
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
GHCR_IMAGE: ghcr.io/${{ github.repository }}
|
||||
DOCKERHUB_IMAGE: docker.io/builderz-labs/mission-control
|
||||
DOCKERHUB_ENABLED: ${{ secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
|
|
@ -48,15 +49,24 @@ jobs:
|
|||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
if: env.DOCKERHUB_ENABLED == 'true'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
images: |
|
||||
${{ env.GHCR_IMAGE }}
|
||||
name=${{ env.DOCKERHUB_IMAGE }},enable=${{ env.DOCKERHUB_ENABLED }}
|
||||
tags: |
|
||||
type=sha,prefix=sha-
|
||||
type=ref,event=branch
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ RUN pnpm build
|
|||
FROM node:22.22.0-slim AS runtime
|
||||
|
||||
ARG MC_VERSION=dev
|
||||
LABEL org.opencontainers.image.source="https://github.com/openclaw/mission-control"
|
||||
LABEL org.opencontainers.image.source="https://github.com/builderz-labs/mission-control"
|
||||
LABEL org.opencontainers.image.description="Mission Control - operations dashboard"
|
||||
LABEL org.opencontainers.image.licenses="MIT"
|
||||
LABEL org.opencontainers.image.version="${MC_VERSION}"
|
||||
|
|
|
|||
|
|
@ -123,6 +123,10 @@ docker compose up
|
|||
|
||||
No `.env` file needed. The container auto-generates `AUTH_SECRET` and `API_KEY` on first boot and persists them across restarts. Visit `http://localhost:3000` to create your admin account.
|
||||
|
||||
Release automation publishes multi-arch images to:
|
||||
- `ghcr.io/builderz-labs/mission-control`
|
||||
- `docker.io/builderz-labs/mission-control` when `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` are configured in GitHub Actions secrets
|
||||
|
||||
### Docker Hardening (Production)
|
||||
|
||||
For production deployments, use the hardened compose overlay:
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@ export async function POST(request: NextRequest) {
|
|||
// Using OpenClaw's sessions_spawn function via clawdbot CLI
|
||||
const spawnPayload = {
|
||||
task,
|
||||
model,
|
||||
label,
|
||||
...(model ? { model } : {}),
|
||||
runTimeoutSeconds: timeout,
|
||||
tools: {
|
||||
profile: getPreferredToolsProfile(),
|
||||
|
|
@ -91,7 +91,7 @@ export async function POST(request: NextRequest) {
|
|||
actor_id: auth.user.id,
|
||||
detail: {
|
||||
spawnId,
|
||||
model,
|
||||
model: model ?? null,
|
||||
label,
|
||||
task_summary: task.length > 120 ? task.slice(0, 120) + '...' : task,
|
||||
toolsProfile: getPreferredToolsProfile(),
|
||||
|
|
@ -105,7 +105,7 @@ export async function POST(request: NextRequest) {
|
|||
spawnId,
|
||||
sessionInfo,
|
||||
task,
|
||||
model,
|
||||
model: model ?? null,
|
||||
label,
|
||||
timeoutSeconds: timeout,
|
||||
createdAt: Date.now(),
|
||||
|
|
@ -124,7 +124,7 @@ export async function POST(request: NextRequest) {
|
|||
spawnId,
|
||||
error: execError.message || 'Failed to spawn agent',
|
||||
task,
|
||||
model,
|
||||
model: model ?? null,
|
||||
label,
|
||||
timeoutSeconds: timeout,
|
||||
createdAt: Date.now()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/lib/config', () => ({
|
||||
config: { openclawConfigPath: '' },
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/logger', () => ({
|
||||
logger: { error: vi.fn(), info: vi.fn(), warn: vi.fn() },
|
||||
}))
|
||||
|
||||
describe('registerMcAsDashboard', () => {
|
||||
const originalEnv = { ...process.env }
|
||||
let tempDir = ''
|
||||
let configPath = ''
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = mkdtempSync(path.join(os.tmpdir(), 'mc-gateway-runtime-'))
|
||||
configPath = path.join(tempDir, 'openclaw.json')
|
||||
process.env = { ...originalEnv }
|
||||
|
||||
const { config } = await import('@/lib/config')
|
||||
config.openclawConfigPath = configPath
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
rmSync(tempDir, { recursive: true, force: true })
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('adds the Mission Control origin without disabling device auth', async () => {
|
||||
writeFileSync(configPath, JSON.stringify({
|
||||
gateway: {
|
||||
controlUi: {
|
||||
allowedOrigins: ['https://existing.example.com'],
|
||||
dangerouslyDisableDeviceAuth: false,
|
||||
},
|
||||
},
|
||||
}, null, 2) + '\n', 'utf-8')
|
||||
|
||||
const { registerMcAsDashboard } = await import('@/lib/gateway-runtime')
|
||||
const result = registerMcAsDashboard('https://mc.example.com/dashboard')
|
||||
|
||||
expect(result).toEqual({ registered: true, alreadySet: false })
|
||||
|
||||
const updated = JSON.parse(readFileSync(configPath, 'utf-8'))
|
||||
expect(updated.gateway.controlUi.allowedOrigins).toEqual([
|
||||
'https://existing.example.com',
|
||||
'https://mc.example.com',
|
||||
])
|
||||
expect(updated.gateway.controlUi.dangerouslyDisableDeviceAuth).toBe(false)
|
||||
})
|
||||
|
||||
it('does not rewrite config when the origin is already present', async () => {
|
||||
writeFileSync(configPath, JSON.stringify({
|
||||
gateway: {
|
||||
controlUi: {
|
||||
allowedOrigins: ['https://mc.example.com'],
|
||||
dangerouslyDisableDeviceAuth: false,
|
||||
},
|
||||
},
|
||||
}, null, 2) + '\n', 'utf-8')
|
||||
|
||||
const before = readFileSync(configPath, 'utf-8')
|
||||
const { registerMcAsDashboard } = await import('@/lib/gateway-runtime')
|
||||
const result = registerMcAsDashboard('https://mc.example.com/sessions')
|
||||
const after = readFileSync(configPath, 'utf-8')
|
||||
|
||||
expect(result).toEqual({ registered: false, alreadySet: true })
|
||||
expect(after).toBe(before)
|
||||
})
|
||||
})
|
||||
|
|
@ -145,7 +145,6 @@ describe('createAlertSchema', () => {
|
|||
describe('spawnAgentSchema', () => {
|
||||
const validSpawn = {
|
||||
task: 'Do something',
|
||||
model: 'sonnet',
|
||||
label: 'worker-1',
|
||||
}
|
||||
|
||||
|
|
@ -157,6 +156,14 @@ describe('spawnAgentSchema', () => {
|
|||
}
|
||||
})
|
||||
|
||||
it('accepts an explicit model when provided', () => {
|
||||
const result = spawnAgentSchema.safeParse({ ...validSpawn, model: 'sonnet' })
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.model).toBe('sonnet')
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects timeout below minimum (10)', () => {
|
||||
const result = spawnAgentSchema.safeParse({ ...validSpawn, timeoutSeconds: 5 })
|
||||
expect(result.success).toBe(false)
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export const qualityReviewSchema = z.object({
|
|||
|
||||
export const spawnAgentSchema = z.object({
|
||||
task: z.string().min(1, 'Task is required'),
|
||||
model: z.string().min(1, 'Model is required'),
|
||||
model: z.string().min(1, 'Model is required').optional(),
|
||||
label: z.string().min(1, 'Label is required'),
|
||||
timeoutSeconds: z.number().min(10).max(3600).default(300),
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue