diff options
| -rw-r--r-- | frontend/src/components/ContractCreateModal.tsx | 185 | ||||
| -rw-r--r-- | frontend/src/components/ContractDetail.tsx | 139 | ||||
| -rw-r--r-- | frontend/src/components/ContractList.tsx | 61 | ||||
| -rw-r--r-- | frontend/src/styles/pc98.css | 644 | ||||
| -rw-r--r-- | frontend/src/types.ts | 95 | ||||
| -rw-r--r-- | frontend/tsconfig.tsbuildinfo | 2 | ||||
| -rw-r--r-- | makima/src/bin/makima.rs | 13 | ||||
| -rw-r--r-- | makima/src/daemon/api/mod.rs | 2 | ||||
| -rw-r--r-- | makima/src/daemon/api/red_team.rs | 39 | ||||
| -rw-r--r-- | makima/src/daemon/cli/mod.rs | 57 | ||||
| -rw-r--r-- | makima/src/daemon/cli/red_team.rs | 26 |
11 files changed, 1238 insertions, 25 deletions
diff --git a/frontend/src/components/ContractCreateModal.tsx b/frontend/src/components/ContractCreateModal.tsx new file mode 100644 index 0000000..e1d9732 --- /dev/null +++ b/frontend/src/components/ContractCreateModal.tsx @@ -0,0 +1,185 @@ +import React, { useState } from 'react' + +interface ContractCreateModalProps { + isOpen: boolean + onClose: () => void + onCreated: () => void +} + +interface CreateContractForm { + name: string + description: string + contractType: string + redTeamEnabled: boolean + redTeamPrompt: string +} + +export function ContractCreateModal({ isOpen, onClose, onCreated }: ContractCreateModalProps) { + const [form, setForm] = useState<CreateContractForm>({ + name: '', + description: '', + contractType: 'simple', + redTeamEnabled: false, + redTeamPrompt: '', + }) + const [loading, setLoading] = useState(false) + const [error, setError] = useState<string | null>(null) + + if (!isOpen) return null + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setLoading(true) + setError(null) + + try { + const response = await fetch('/api/v1/contracts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: form.name, + description: form.description || undefined, + contract_type: form.contractType, + red_team_enabled: form.redTeamEnabled, + red_team_prompt: form.redTeamEnabled && form.redTeamPrompt ? form.redTeamPrompt : undefined, + }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.message || `Failed to create contract: ${response.statusText}`) + } + + // Reset form and close modal + setForm({ + name: '', + description: '', + contractType: 'simple', + redTeamEnabled: false, + redTeamPrompt: '', + }) + onCreated() + onClose() + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error') + } finally { + setLoading(false) + } + } + + return ( + <div className="modal-overlay" onClick={onClose}> + <div className="settings-modal contract-create-modal" onClick={(e) => e.stopPropagation()}> + <div className="modal-header"> + <h2 className="modal-title">Create Contract</h2> + <button className="modal-close-btn" onClick={onClose}> + à + </button> + </div> + + <form onSubmit={handleSubmit}> + <div className="modal-content"> + {error && ( + <div className="form-error"> + {error} + </div> + )} + + <div className="settings-section"> + <h3>Contract Details</h3> + + <div className="setting-item"> + <label> + Name + <input + type="text" + value={form.name} + onChange={(e) => setForm({ ...form, name: e.target.value })} + placeholder="Enter contract name" + required + className="form-input" + /> + </label> + </div> + + <div className="setting-item"> + <label> + Description + <textarea + value={form.description} + onChange={(e) => setForm({ ...form, description: e.target.value })} + placeholder="Enter contract description (optional)" + className="form-textarea" + rows={3} + /> + </label> + </div> + + <div className="setting-item"> + <label> + Contract Type + <select + value={form.contractType} + onChange={(e) => setForm({ ...form, contractType: e.target.value })} + className="form-select" + > + <option value="simple">Simple</option> + <option value="specification">Specification</option> + <option value="execute">Execute</option> + </select> + </label> + </div> + </div> + + <div className="settings-section"> + <h3>Red Team Monitoring</h3> + + <div className="setting-item"> + <label className="checkbox-label"> + <input + type="checkbox" + checked={form.redTeamEnabled} + onChange={(e) => setForm({ ...form, redTeamEnabled: e.target.checked })} + /> + <span>Enable Red Team monitoring</span> + </label> + <div className="setting-description"> + Spawns a parallel task that monitors work output for quality and compliance + </div> + </div> + + {form.redTeamEnabled && ( + <div className="setting-item red-team-prompt-container"> + <label> + Custom review criteria (optional) + <textarea + value={form.redTeamPrompt} + onChange={(e) => setForm({ ...form, redTeamPrompt: e.target.value })} + placeholder="Enter custom criteria for the red team to evaluate against... (e.g., 'Ensure all functions have proper error handling', 'Verify security best practices are followed')" + className="form-textarea" + rows={4} + /> + </label> + <div className="setting-description"> + Provide specific criteria for the red team to focus on during reviews + </div> + </div> + )} + </div> + </div> + + <div className="modal-footer"> + <button type="button" className="modal-btn secondary" onClick={onClose} disabled={loading}> + Cancel + </button> + <button type="submit" className="modal-btn primary" disabled={loading || !form.name.trim()}> + {loading ? 'Creating...' : 'Create Contract'} + </button> + </div> + </form> + </div> + </div> + ) +} diff --git a/frontend/src/components/ContractDetail.tsx b/frontend/src/components/ContractDetail.tsx index 72527ce..a9dd550 100644 --- a/frontend/src/components/ContractDetail.tsx +++ b/frontend/src/components/ContractDetail.tsx @@ -12,6 +12,8 @@ interface TaskSummary { id: string name: string status: string + is_supervisor?: boolean + is_red_team?: boolean } interface ContractRepository { @@ -30,6 +32,24 @@ interface Contract { status: string version: number created_at: string + red_team_enabled?: boolean + red_team_prompt?: string +} + +interface RedTeamNotification { + id: string + contract_id: string + red_team_task_id: string + related_task_id?: string + message: string + severity: 'info' | 'warning' | 'critical' + file_path?: string + context?: string + delivered: boolean + delivered_at?: string + acknowledged: boolean + acknowledged_at?: string + created_at: string } interface ContractWithRelations { @@ -39,7 +59,7 @@ interface ContractWithRelations { tasks: TaskSummary[] } -type Tab = 'overview' | 'files' | 'tasks' | 'repositories' +type Tab = 'overview' | 'files' | 'tasks' | 'repositories' | 'red-team' export function ContractDetail() { const { id } = useParams<{ id: string }>() @@ -47,6 +67,8 @@ export function ContractDetail() { const [loading, setLoading] = useState(true) const [error, setError] = useState<string | null>(null) const [activeTab, setActiveTab] = useState<Tab>('overview') + const [notifications, setNotifications] = useState<RedTeamNotification[]>([]) + const [notificationsLoading, setNotificationsLoading] = useState(false) useEffect(() => { async function fetchContract() { @@ -70,6 +92,28 @@ export function ContractDetail() { fetchContract() }, [id]) + // Fetch red team notifications when viewing red-team tab + useEffect(() => { + async function fetchNotifications() { + if (!id || activeTab !== 'red-team' || !data?.contract.red_team_enabled) return + + try { + setNotificationsLoading(true) + const response = await fetch(`/api/v1/contracts/${id}/red-team/notifications`) + if (response.ok) { + const notificationData = await response.json() + setNotifications(notificationData.notifications || []) + } + } catch (err) { + console.error('Failed to fetch red team notifications:', err) + } finally { + setNotificationsLoading(false) + } + } + + fetchNotifications() + }, [id, activeTab, data?.contract.red_team_enabled]) + if (loading) { return ( <div className="contract-detail-container"> @@ -108,7 +152,14 @@ export function ContractDetail() { <Link to="/contracts" className="back-link"> Back to Contracts </Link> - <h1 className="contract-title">{contract.name}</h1> + <h1 className="contract-title"> + {contract.name} + {contract.red_team_enabled && ( + <span className="red-team-badge" title="Red Team monitoring enabled"> + đĄī¸ Red Team + </span> + )} + </h1> {contract.description && ( <p className="contract-description">{contract.description}</p> )} @@ -144,6 +195,14 @@ export function ContractDetail() { > Repositories ({repositories.length}) </button> + {contract.red_team_enabled && ( + <button + className={`tab-button red-team-tab ${activeTab === 'red-team' ? 'active' : ''}`} + onClick={() => setActiveTab('red-team')} + > + đĄī¸ Red Team + </button> + )} </div> <div className="contract-tab-content"> @@ -194,8 +253,12 @@ export function ContractDetail() { ) : ( <ul className="task-list"> {tasks.map((task) => ( - <li key={task.id} className="task-item"> - <h3>{task.name}</h3> + <li key={task.id} className={`task-item ${task.is_red_team ? 'red-team-task' : ''}`}> + <h3> + {task.is_red_team && <span className="task-badge red-team">đĄī¸</span>} + {task.is_supervisor && <span className="task-badge supervisor">đ</span>} + {task.name} + </h3> <span className={`task-status status-${task.status}`}> {task.status} </span> @@ -226,6 +289,74 @@ export function ContractDetail() { )} </div> )} + + {activeTab === 'red-team' && contract.red_team_enabled && ( + <div className="tab-panel red-team-panel"> + <h2>đĄī¸ Red Team Monitoring</h2> + + {contract.red_team_prompt && ( + <div className="red-team-prompt"> + <h3>Review Criteria</h3> + <p>{contract.red_team_prompt}</p> + </div> + )} + + <div className="red-team-status"> + <h3>Red Team Task</h3> + {(() => { + const redTeamTask = tasks.find(t => t.is_red_team) + if (redTeamTask) { + return ( + <div className="red-team-task-info"> + <span className="task-name">{redTeamTask.name}</span> + <span className={`task-status status-${redTeamTask.status}`}> + {redTeamTask.status} + </span> + </div> + ) + } + return <p className="no-task">Red team task not yet spawned</p> + })()} + </div> + + <div className="red-team-notifications"> + <h3>Alerts ({notifications.length})</h3> + {notificationsLoading ? ( + <p>Loading notifications...</p> + ) : notifications.length === 0 ? ( + <p className="no-alerts">No alerts from red team</p> + ) : ( + <ul className="notification-list"> + {notifications.map((notification) => ( + <li + key={notification.id} + className={`notification-item severity-${notification.severity}`} + > + <div className="notification-header"> + <span className={`severity-badge ${notification.severity}`}> + {notification.severity === 'critical' && 'đ¨'} + {notification.severity === 'warning' && 'â ī¸'} + {notification.severity === 'info' && 'âšī¸'} + {notification.severity.toUpperCase()} + </span> + <span className="notification-time"> + {new Date(notification.created_at).toLocaleString()} + </span> + </div> + <p className="notification-message">{notification.message}</p> + {notification.file_path && ( + <span className="notification-file">File: {notification.file_path}</span> + )} + {notification.context && ( + <pre className="notification-context">{notification.context}</pre> + )} + </li> + ))} + </ul> + )} + </div> + </div> + )} </div> </div> ) diff --git a/frontend/src/components/ContractList.tsx b/frontend/src/components/ContractList.tsx index 77012db..253b44f 100644 --- a/frontend/src/components/ContractList.tsx +++ b/frontend/src/components/ContractList.tsx @@ -1,5 +1,6 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useCallback } from 'react' import { Link } from 'react-router-dom' +import { ContractCreateModal } from './ContractCreateModal' interface ContractSummary { id: string @@ -12,32 +13,35 @@ interface ContractSummary { task_count: number repository_count: number created_at: string + // Red team fields + red_team_enabled?: boolean } export function ContractList() { const [contracts, setContracts] = useState<ContractSummary[]>([]) const [loading, setLoading] = useState(true) const [error, setError] = useState<string | null>(null) + const [showCreateModal, setShowCreateModal] = useState(false) - useEffect(() => { - async function fetchContracts() { - try { - setLoading(true) - const response = await fetch('/api/v1/contracts') - if (!response.ok) { - throw new Error(`Failed to fetch contracts: ${response.statusText}`) - } - const data = await response.json() - setContracts(data.contracts || []) - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error') - } finally { - setLoading(false) + const fetchContracts = useCallback(async () => { + try { + setLoading(true) + const response = await fetch('/api/v1/contracts') + if (!response.ok) { + throw new Error(`Failed to fetch contracts: ${response.statusText}`) } + const data = await response.json() + setContracts(data.contracts || []) + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error') + } finally { + setLoading(false) } + }, []) + useEffect(() => { fetchContracts() - }, []) + }, [fetchContracts]) if (loading) { return ( @@ -57,7 +61,15 @@ export function ContractList() { return ( <div className="contract-list-container"> - <h1>Contracts</h1> + <div className="contract-list-header"> + <h1>Contracts</h1> + <button + className="create-contract-btn" + onClick={() => setShowCreateModal(true)} + > + + New Contract + </button> + </div> {contracts.length === 0 ? ( <p>No contracts found</p> ) : ( @@ -65,7 +77,14 @@ export function ContractList() { {contracts.map((contract) => ( <li key={contract.id} className="contract-item"> <Link to={`/contracts/${contract.id}`}> - <h2>{contract.name}</h2> + <h2> + {contract.name} + {contract.red_team_enabled && ( + <span className="red-team-badge" title="Red Team monitoring enabled"> + đ + </span> + )} + </h2> {contract.description && <p>{contract.description}</p>} <div className="contract-meta"> <span>Phase: {contract.phase}</span> @@ -78,6 +97,12 @@ export function ContractList() { ))} </ul> )} + + <ContractCreateModal + isOpen={showCreateModal} + onClose={() => setShowCreateModal(false)} + onCreated={fetchContracts} + /> </div> ) } diff --git a/frontend/src/styles/pc98.css b/frontend/src/styles/pc98.css index 4dcf15e..a420115 100644 --- a/frontend/src/styles/pc98.css +++ b/frontend/src/styles/pc98.css @@ -4621,3 +4621,647 @@ button:focus-visible { display: none; } } + +/* ============================================================================= + Contract List & Detail Styles + ============================================================================= */ + +.contract-list-container, +.contract-detail-container { + padding: 20px; + max-width: 1200px; + margin: 0 auto; + font-family: 'MS Gothic', monospace; + color: #ffffff; +} + +.contract-list-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +.contract-list-header h1 { + font-size: 24px; + color: #66ccff; + margin: 0; + text-transform: uppercase; + letter-spacing: 1px; +} + +.create-contract-btn { + appearance: none; + background: linear-gradient(180deg, #004466, #002233); + border: 2px solid #66ccff; + color: #ffffff; + font-family: 'MS Gothic', monospace; + font-size: 13px; + font-weight: bold; + padding: 8px 16px; + cursor: pointer; + border-radius: 4px; + transition: all 0.2s ease; +} + +.create-contract-btn:hover { + background: linear-gradient(180deg, #005577, #003344); + box-shadow: 0 0 10px rgba(102, 204, 255, 0.4); +} + +.contract-list { + list-style: none; + padding: 0; + margin: 0; +} + +.contract-item { + margin-bottom: 15px; + border: 1px solid rgba(102, 204, 255, 0.3); + background: rgba(0, 0, 0, 0.5); + transition: all 0.3s ease; +} + +.contract-item:hover { + border-color: #66ccff; + background: rgba(102, 204, 255, 0.05); +} + +.contract-item a { + display: block; + padding: 15px; + text-decoration: none; + color: inherit; +} + +.contract-item h2 { + font-size: 16px; + color: #ffffff; + margin: 0 0 8px 0; + display: flex; + align-items: center; + gap: 10px; +} + +.contract-item p { + font-size: 13px; + color: rgba(255, 255, 255, 0.7); + margin: 0 0 10px 0; +} + +.contract-meta { + display: flex; + flex-wrap: wrap; + gap: 15px; + font-size: 12px; + color: rgba(102, 204, 255, 0.8); +} + +/* ============================================================================= + Red Team Styling + ============================================================================= */ + +/* Red Team Badge - Contract List and Detail */ +.red-team-badge { + display: inline-flex; + align-items: center; + gap: 4px; + margin-left: 8px; + padding: 2px 8px; + font-size: 12px; + font-weight: normal; + background: linear-gradient(180deg, #4a0000, #2d0000); + border: 1px solid #ff4444; + border-radius: 4px; + color: #ff6666; + vertical-align: middle; + box-shadow: 0 0 6px rgba(255, 68, 68, 0.3); +} + +/* Contract Detail Header */ +.contract-detail-header { + margin-bottom: 20px; +} + +.back-link { + display: inline-block; + font-size: 12px; + color: #66ccff; + text-decoration: none; + margin-bottom: 15px; + transition: opacity 0.3s ease; +} + +.back-link:hover { + opacity: 0.8; +} + +.contract-title { + display: flex; + align-items: center; + gap: 15px; + flex-wrap: wrap; + font-size: 24px; + color: #66ccff; + margin: 0 0 10px 0; +} + +.contract-description { + font-size: 14px; + color: rgba(255, 255, 255, 0.7); + margin: 10px 0; +} + +/* Contract Tabs */ +.contract-tabs { + display: flex; + flex-wrap: wrap; + gap: 5px; + margin-bottom: 20px; + border-bottom: 1px solid rgba(102, 204, 255, 0.3); + padding-bottom: 10px; +} + +.tab-button { + font-family: 'MS Gothic', monospace; + font-size: 12px; + padding: 8px 16px; + border: 1px solid rgba(102, 204, 255, 0.3); + background: rgba(0, 0, 0, 0.3); + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + transition: all 0.3s ease; +} + +.tab-button:hover { + border-color: #66ccff; + color: #ffffff; +} + +.tab-button.active { + background: rgba(102, 204, 255, 0.15); + border-color: #66ccff; + color: #66ccff; +} + +/* Red Team Tab Button */ +.tab-button.red-team-tab { + background: linear-gradient(180deg, #3d0000, #1a0000); + border-color: #ff4444; + color: #ff6666; +} + +.tab-button.red-team-tab:hover { + background: linear-gradient(180deg, #4a0000, #2d0000); + box-shadow: 0 0 8px rgba(255, 68, 68, 0.4); +} + +.tab-button.red-team-tab.active { + background: linear-gradient(180deg, #5c0000, #3d0000); + border-color: #ff6666; + color: #ffffff; + box-shadow: 0 0 12px rgba(255, 68, 68, 0.5); +} + +/* Tab Content */ +.tab-panel { + padding: 20px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(102, 204, 255, 0.2); +} + +.tab-panel h2 { + font-size: 18px; + color: #66ccff; + margin: 0 0 15px 0; + text-transform: uppercase; + letter-spacing: 1px; +} + +.overview-list { + display: grid; + grid-template-columns: 120px 1fr; + gap: 10px; +} + +.overview-list dt { + color: rgba(255, 255, 255, 0.6); + font-size: 13px; +} + +.overview-list dd { + color: #ffffff; + margin: 0; + font-size: 13px; +} + +/* File/Task/Repository Lists */ +.file-list, +.task-list, +.repository-list { + list-style: none; + padding: 0; + margin: 0; +} + +.file-item, +.task-item, +.repository-item { + padding: 12px; + margin-bottom: 10px; + border: 1px solid rgba(102, 204, 255, 0.2); + background: rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; +} + +.file-item:hover, +.task-item:hover, +.repository-item:hover { + border-color: rgba(102, 204, 255, 0.4); +} + +.file-item a { + text-decoration: none; + color: inherit; +} + +.file-item h3, +.task-item h3, +.repository-item h3 { + font-size: 14px; + color: #ffffff; + margin: 0 0 5px 0; + display: flex; + align-items: center; + gap: 8px; +} + +.file-phase { + font-size: 11px; + color: rgba(102, 204, 255, 0.7); +} + +.task-status { + font-size: 11px; + padding: 2px 8px; + border-radius: 2px; +} + +.task-status.status-pending { + background: rgba(255, 255, 0, 0.15); + color: #ffff00; + border: 1px solid rgba(255, 255, 0, 0.3); +} + +.task-status.status-active, +.task-status.status-running { + background: rgba(0, 255, 0, 0.15); + color: #00ff00; + border: 1px solid rgba(0, 255, 0, 0.3); +} + +.task-status.status-completed { + background: rgba(102, 204, 255, 0.15); + color: #66ccff; + border: 1px solid rgba(102, 204, 255, 0.3); +} + +.task-status.status-failed { + background: rgba(255, 100, 100, 0.15); + color: #ff6464; + border: 1px solid rgba(255, 100, 100, 0.3); +} + +/* Task Badges */ +.task-badge { + margin-right: 6px; + font-size: 14px; +} + +.task-badge.red-team { + color: #ff6666; +} + +.task-badge.supervisor { + color: #66ccff; +} + +.task-item.red-team-task { + border-left: 3px solid #ff4444; +} + +.primary-badge { + font-size: 10px; + padding: 2px 6px; + background: rgba(102, 204, 255, 0.15); + color: #66ccff; + border: 1px solid rgba(102, 204, 255, 0.3); + margin-left: 8px; +} + +.repo-type { + font-size: 11px; + color: rgba(255, 255, 255, 0.6); +} + +/* Red Team Panel */ +.red-team-panel { + background: linear-gradient(180deg, rgba(60, 0, 0, 0.3), rgba(30, 0, 0, 0.3)); + border-left: 3px solid #ff4444; +} + +.red-team-panel h2 { + color: #ff6666; + border-bottom: 2px solid #ff4444; + padding-bottom: 8px; +} + +.red-team-panel h3 { + color: #ffaaaa; + margin-top: 20px; + margin-bottom: 12px; +} + +/* Red Team Prompt Section */ +.red-team-prompt { + background: rgba(0, 0, 0, 0.4); + border: 1px solid #ff4444; + border-radius: 6px; + padding: 12px 16px; + margin-bottom: 16px; +} + +.red-team-prompt h3 { + margin-top: 0; + font-size: 14px; +} + +.red-team-prompt p { + margin: 0; + color: #cccccc; + font-style: italic; +} + +/* Red Team Status */ +.red-team-status { + margin-bottom: 20px; +} + +.red-team-task-info { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid #444; + border-radius: 4px; +} + +.red-team-task-info .task-name { + font-weight: bold; + color: #ffffff; +} + +.no-task, .no-alerts { + color: #888888; + font-style: italic; +} + +/* Red Team Notifications */ +.red-team-notifications { + margin-top: 20px; +} + +.notification-list { + list-style: none; + padding: 0; + margin: 0; +} + +.notification-item { + padding: 12px 16px; + margin-bottom: 12px; + background: rgba(0, 0, 0, 0.4); + border-radius: 6px; + border-left: 4px solid; +} + +.notification-item.severity-info { + border-left-color: #66ccff; + background: linear-gradient(90deg, rgba(0, 80, 150, 0.2), transparent); +} + +.notification-item.severity-warning { + border-left-color: #ffcc00; + background: linear-gradient(90deg, rgba(150, 120, 0, 0.2), transparent); +} + +.notification-item.severity-critical { + border-left-color: #ff4444; + background: linear-gradient(90deg, rgba(150, 0, 0, 0.2), transparent); +} + +.notification-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.severity-badge { + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: bold; + letter-spacing: 0.5px; +} + +.severity-badge.info { + background: #003366; + color: #66ccff; + border: 1px solid #66ccff; +} + +.severity-badge.warning { + background: #333300; + color: #ffcc00; + border: 1px solid #ffcc00; +} + +.severity-badge.critical { + background: #660000; + color: #ff4444; + border: 1px solid #ff4444; + animation: pulse-critical 2s infinite; +} + +@keyframes pulse-critical { + 0%, 100% { + box-shadow: 0 0 4px rgba(255, 68, 68, 0.5); + } + 50% { + box-shadow: 0 0 12px rgba(255, 68, 68, 0.8); + } +} + +.notification-time { + font-size: 11px; + color: #888888; +} + +.notification-message { + margin: 0 0 8px 0; + color: #ffffff; + line-height: 1.5; +} + +.notification-file { + display: block; + font-size: 12px; + color: #66ccff; + margin-bottom: 8px; +} + +.notification-context { + margin: 8px 0 0 0; + padding: 8px 12px; + background: rgba(0, 0, 0, 0.5); + border-radius: 4px; + font-size: 12px; + font-family: 'MS Gothic', monospace; + color: #aaaaaa; + white-space: pre-wrap; + overflow-x: auto; +} + +/* ============================================================================= + Contract Create Modal Styles + ============================================================================= */ + +.contract-create-modal { + max-width: 600px; +} + +.contract-create-modal .settings-section:last-child { + border-top: 1px solid #444; + padding-top: 16px; + margin-top: 8px; +} + +.form-input, +.form-textarea, +.form-select { + width: 100%; + padding: 8px 12px; + margin-top: 6px; + background: rgba(0, 0, 0, 0.4); + border: 1px solid #444; + border-radius: 4px; + color: #ffffff; + font-family: 'MS Gothic', monospace; + font-size: 13px; +} + +.form-input:focus, +.form-textarea:focus, +.form-select:focus { + outline: none; + border-color: #66ccff; + box-shadow: 0 0 6px rgba(102, 204, 255, 0.3); +} + +.form-textarea { + resize: vertical; + min-height: 80px; +} + +.form-select { + cursor: pointer; +} + +.form-select option { + background: #000000; + color: #ffffff; +} + +.form-error { + padding: 10px 14px; + margin-bottom: 16px; + background: rgba(255, 0, 0, 0.15); + border: 1px solid #ff4444; + border-radius: 4px; + color: #ff6666; + font-size: 13px; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.checkbox-label span { + color: #ffffff; +} + +.setting-description { + margin-top: 4px; + font-size: 11px; + color: #888888; + padding-left: 24px; +} + +.red-team-prompt-container { + margin-top: 12px; + padding-left: 20px; +} + +.red-team-prompt-container textarea { + background: rgba(60, 0, 0, 0.2); + border-color: #ff4444; +} + +.red-team-prompt-container textarea:focus { + border-color: #ff6666; + box-shadow: 0 0 8px rgba(255, 68, 68, 0.3); +} + +.modal-btn.primary { + background: linear-gradient(180deg, #004466, #002233); + border-color: #66ccff; + color: #ffffff; +} + +.modal-btn.primary:hover { + background: linear-gradient(180deg, #005577, #003344); + box-shadow: 0 0 8px rgba(102, 204, 255, 0.4); +} + +.modal-btn.primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.modal-btn.secondary { + background: linear-gradient(180deg, #333333, #1a1a1a); + border-color: #666666; + color: #cccccc; +} + +.modal-btn.secondary:hover { + background: linear-gradient(180deg, #444444, #2a2a2a); +} + +.loading, +.error, +.not-found { + padding: 40px; + text-align: center; + font-size: 14px; + color: rgba(255, 255, 255, 0.7); +} + +.error { + color: #ff6464; +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index c6d1263..ac8d417 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -9,3 +9,98 @@ export type Choice = { id: string label: string } + +// Contract types +export type ContractType = 'simple' | 'specification' | 'execute' +export type ContractPhase = 'research' | 'specify' | 'plan' | 'execute' | 'review' +export type ContractStatus = 'active' | 'completed' | 'archived' + +export interface ContractSummary { + id: string + name: string + description?: string + contractType: string + phase: string + status: string + supervisorTaskId?: string + localOnly: boolean + fileCount: number + taskCount: number + repositoryCount: number + version: number + createdAt: string + // Red team fields + redTeamEnabled?: boolean +} + +export interface Contract { + id: string + ownerId: string + name: string + description?: string + contractType: string + phase: string + status: string + supervisorTaskId?: string + autonomousLoop: boolean + phaseGuard: boolean + completedDeliverables: Record<string, string[]> + localOnly: boolean + redTeamEnabled: boolean + redTeamPrompt?: string + version: number + createdAt: string + updatedAt: string +} + +export interface CreateContractRequest { + name: string + description?: string + contractType?: string + initialPhase?: string + autonomousLoop?: boolean + phaseGuard?: boolean + localOnly?: boolean + redTeamEnabled?: boolean + redTeamPrompt?: string +} + +export interface TaskSummary { + id: string + contractId?: string + contractName?: string + contractPhase?: string + contractStatus?: string + parentTaskId?: string + depth: number + name: string + status: string + priority: number + progressSummary?: string + subtaskCount: number + version: number + isSupervisor: boolean + isRedTeam: boolean + hidden: boolean + createdAt: string + updatedAt: string +} + +// Red team notification types +export type NotificationSeverity = 'info' | 'warning' | 'critical' + +export interface RedTeamNotification { + id: string + contractId: string + redTeamTaskId: string + relatedTaskId?: string + message: string + severity: NotificationSeverity + filePath?: string + context?: string + delivered: boolean + deliveredAt?: string + acknowledged: boolean + acknowledgedAt?: string + createdAt: string +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 79408dc..0fd5af9 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/contractdetail.tsx","./src/components/contractlist.tsx","./src/components/dialoguebox.tsx","./src/components/filedetail.tsx","./src/components/heartlogo.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/origamidragonlogo.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"}
\ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/bottombar.tsx","./src/components/choicemenu.tsx","./src/components/cityscapebackground.tsx","./src/components/configmodal.tsx","./src/components/contractcreatemodal.tsx","./src/components/contractdetail.tsx","./src/components/contractlist.tsx","./src/components/dialoguebox.tsx","./src/components/filedetail.tsx","./src/components/heartlogo.tsx","./src/components/landingpage.tsx","./src/components/loadingscreen.tsx","./src/components/origamidragonlogo.tsx","./src/components/topbar.tsx","./src/components/vnapp.tsx","./src/components/vninterface.tsx","./src/components/vnviewport.tsx","./src/services/ws.ts","./src/stores/index.ts"],"version":"5.9.2"}
\ No newline at end of file diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index ffb9364..44fa590 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use makima::daemon::api::{ApiClient, CreateContractRequest}; use makima::daemon::cli::{ - Cli, CliConfig, Commands, ConfigCommand, ContractCommand, SupervisorCommand, ViewArgs, + Cli, CliConfig, Commands, ConfigCommand, ContractCommand, RedTeamCommand, SupervisorCommand, ViewArgs, }; use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion}; use makima::daemon::config::{DaemonConfig, RepoEntry}; @@ -30,6 +30,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { Commands::Contract(cmd) => run_contract(cmd).await, Commands::View(args) => run_view(args).await, Commands::Config(cmd) => run_config(cmd).await, + Commands::RedTeam(cmd) => run_red_team(cmd).await, } } @@ -789,6 +790,16 @@ async fn run_config(cmd: ConfigCommand) -> Result<(), Box<dyn std::error::Error } } +/// Run red team commands. +async fn run_red_team(cmd: RedTeamCommand) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + match cmd { + RedTeamCommand::Notify(args) => { + makima::daemon::cli::handle_notify(args).await?; + Ok(()) + } + } +} + /// Load contracts from API async fn load_contracts(client: &ApiClient) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> { let result = client.list_contracts().await?; diff --git a/makima/src/daemon/api/mod.rs b/makima/src/daemon/api/mod.rs index 49d80e0..92e34e9 100644 --- a/makima/src/daemon/api/mod.rs +++ b/makima/src/daemon/api/mod.rs @@ -2,7 +2,9 @@ pub mod client; pub mod contract; +pub mod red_team; pub mod supervisor; pub use client::ApiClient; pub use contract::CreateContractRequest; +pub use red_team::RedTeamNotifyRequest; diff --git a/makima/src/daemon/api/red_team.rs b/makima/src/daemon/api/red_team.rs new file mode 100644 index 0000000..6d3c969 --- /dev/null +++ b/makima/src/daemon/api/red_team.rs @@ -0,0 +1,39 @@ +//! Red team API methods. + +use serde::Serialize; +use uuid::Uuid; + +use super::client::{ApiClient, ApiError}; +use super::supervisor::JsonValue; + +/// Request body for red team notify endpoint. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RedTeamNotifyRequest { + /// The issue message + pub message: String, + + /// Severity level: low, medium, high, critical + pub severity: String, + + /// The specific task this relates to (optional) + #[serde(skip_serializing_if = "Option::is_none")] + pub related_task_id: Option<Uuid>, + + /// The file path where the issue was detected (optional) + #[serde(skip_serializing_if = "Option::is_none")] + pub file_path: Option<String>, + + /// Additional context about the issue (optional) + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option<String>, +} + +impl ApiClient { + /// Send a red team notification about an issue found during adversarial review. + /// + /// POST /api/v1/mesh/red-team/notify + pub async fn red_team_notify(&self, req: RedTeamNotifyRequest) -> Result<JsonValue, ApiError> { + self.post("/api/v1/mesh/red-team/notify", &req).await + } +} diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index 0805edd..c848e8e 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -3,15 +3,18 @@ pub mod config; pub mod contract; pub mod daemon; +pub mod red_team; pub mod server; pub mod supervisor; pub mod view; -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; +use uuid::Uuid; pub use config::CliConfig; pub use contract::ContractArgs; pub use daemon::DaemonArgs; +pub use red_team::handle_notify; pub use server::ServerArgs; pub use supervisor::SupervisorArgs; pub use view::ViewArgs; @@ -58,6 +61,10 @@ pub enum Commands { /// Saves configuration to ~/.makima/config.toml for use by CLI commands. #[command(subcommand)] Config(ConfigCommand), + + /// Red team commands for adversarial monitoring + #[command(name = "red-team", subcommand)] + RedTeam(RedTeamCommand), } /// Config subcommands for CLI configuration. @@ -196,6 +203,54 @@ pub enum ContractCommand { CreateFile(contract::CreateFileArgs), } +/// Red team subcommands for adversarial monitoring. +#[derive(Subcommand, Debug)] +pub enum RedTeamCommand { + /// Send a notification to the supervisor about a detected issue. + /// Only available to red team tasks. + Notify(RedTeamNotifyArgs), +} + +/// Arguments for red-team notify command. +#[derive(Args, Debug)] +pub struct RedTeamNotifyArgs { + /// API URL + #[arg(long, env = "MAKIMA_API_URL", default_value = "https://api.makima.jp")] + pub api_url: String, + + /// API key for authentication + #[arg(long, env = "MAKIMA_API_KEY")] + pub api_key: String, + + /// Current task ID (must be a red team task) + #[arg(long, env = "MAKIMA_TASK_ID")] + pub task_id: Uuid, + + /// Contract ID + #[arg(long, env = "MAKIMA_CONTRACT_ID")] + pub contract_id: Uuid, + + /// The notification message + #[arg(index = 1)] + pub message: String, + + /// Severity level: low, medium, high, critical + #[arg(long, default_value = "medium")] + pub severity: String, + + /// Related task ID (optional) + #[arg(long)] + pub task: Option<Uuid>, + + /// Related file path (optional) + #[arg(long)] + pub file: Option<String>, + + /// Additional context (optional) + #[arg(long)] + pub context: Option<String>, +} + impl Cli { /// Parse command-line arguments pub fn parse_args() -> Self { diff --git a/makima/src/daemon/cli/red_team.rs b/makima/src/daemon/cli/red_team.rs new file mode 100644 index 0000000..771aae4 --- /dev/null +++ b/makima/src/daemon/cli/red_team.rs @@ -0,0 +1,26 @@ +//! Red Team subcommand - adversarial review notification commands. + +use crate::daemon::api::{ApiClient, RedTeamNotifyRequest}; +use super::RedTeamNotifyArgs; + +/// Handle the red-team notify command. +pub async fn handle_notify(args: RedTeamNotifyArgs) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + let client = ApiClient::new(args.api_url, args.api_key)?; + + // Use --task if provided, otherwise fall back to MAKIMA_TASK_ID + let related_task_id = args.task; + + let req = RedTeamNotifyRequest { + message: args.message, + severity: args.severity, + related_task_id, + file_path: args.file, + context: args.context, + }; + + eprintln!("Sending red team notification..."); + let result = client.red_team_notify(req).await?; + println!("{}", serde_json::to_string(&result.0)?); + + Ok(()) +} |
