optz: Goal orchestrator
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
||||
import type { GoalJob, GoalStage } from '../../lib/api';
|
||||
import { StatusBadge } from '../ui/Card';
|
||||
|
||||
interface ModelInteractionDiagramProps {
|
||||
goal: GoalJob;
|
||||
localStage?: GoalStage;
|
||||
cloudStage?: GoalStage;
|
||||
}
|
||||
|
||||
const tone = (status: string) => status === 'pass' || status === 'completed' ? 'bg-emerald-500' : status === 'error' || status === 'failed' ? 'bg-rose-500' : status === 'warning' || status === 'degraded' ? 'bg-amber-500' : status === 'running' ? 'animate-pulse bg-indigo-500 motion-reduce:animate-none' : 'bg-slate-300';
|
||||
|
||||
function Exchange({ from, to, label, status, reverse = false }: { from: string; to: string; label: string; status: string; reverse?: boolean }) {
|
||||
return (
|
||||
<div className="grid grid-cols-[minmax(88px,1fr)_minmax(120px,2fr)_minmax(88px,1fr)] items-center gap-2">
|
||||
<div className={`truncate text-xs font-semibold ${reverse ? 'text-right text-slate-500' : 'text-slate-800'}`}>{reverse ? to : from}</div>
|
||||
<div className="min-w-0">
|
||||
<div className={`flex items-center ${reverse ? 'flex-row-reverse' : ''}`}>
|
||||
<span className={`h-2.5 w-2.5 shrink-0 rounded-full ${tone(status)}`} />
|
||||
<span className={`h-px flex-1 ${status === 'error' || status === 'failed' ? 'bg-rose-300' : status === 'warning' ? 'bg-amber-300' : 'bg-indigo-300'}`} />
|
||||
<span className={`h-0 w-0 border-y-4 border-y-transparent ${reverse ? 'border-r-[7px] border-r-indigo-500' : 'border-l-[7px] border-l-indigo-500'}`} />
|
||||
</div>
|
||||
<div className="mt-1 truncate text-center text-[10px] font-medium text-slate-500">{label}</div>
|
||||
</div>
|
||||
<div className={`truncate text-xs font-semibold ${reverse ? 'text-slate-800' : 'text-right text-slate-500'}`}>{reverse ? from : to}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ModelInteractionDiagram({ goal, localStage, cloudStage }: ModelInteractionDiagramProps) {
|
||||
const localStatus = localStage?.status ?? 'queued';
|
||||
const cloudStatus = cloudStage?.status ?? 'queued';
|
||||
return (
|
||||
<section className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm" aria-label="Model interaction diagram">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-100 px-5 py-4">
|
||||
<div><div className="text-[10px] font-bold uppercase tracking-[0.16em] text-indigo-600">Live model exchange</div><h2 className="mt-1 font-semibold text-slate-900">Models trao đổi và kiểm chứng như thế nào</h2></div>
|
||||
<StatusBadge value={goal.status} />
|
||||
</div>
|
||||
<div className="grid gap-4 p-5 lg:grid-cols-[220px_minmax(0,1fr)]">
|
||||
<div className="grid grid-cols-2 gap-2 lg:grid-cols-1">
|
||||
<div className="rounded-xl border border-indigo-100 bg-indigo-50 p-3"><div className="text-[10px] font-bold uppercase tracking-wide text-indigo-500">Local worker</div><div className="mt-1 truncate text-xs font-semibold text-slate-800">{localStage?.model || goal.local_model}</div></div>
|
||||
<div className="rounded-xl border border-violet-100 bg-violet-50 p-3"><div className="text-[10px] font-bold uppercase tracking-wide text-violet-500">Cloud reviewer</div><div className="mt-1 truncate text-xs font-semibold text-slate-800">{cloudStage?.model || goal.cloud_model}</div></div>
|
||||
</div>
|
||||
<div className="space-y-4 rounded-xl border border-slate-200 bg-[radial-gradient(circle_at_top,#eef2ff_0,transparent_55%)] p-4">
|
||||
<Exchange from="Orchestrator" to="Local model" label="Objective + governed context" status={localStatus} />
|
||||
<Exchange from="Local model" to="H4 Security" label="Primary proposal" status={localStatus} />
|
||||
<Exchange from="H4 Security" to="Cloud model" label="Safe draft for independent review" status={cloudStatus} />
|
||||
<Exchange from="Cloud model" to="Orchestrator" label="Critique + refined outcome" status={cloudStatus} reverse />
|
||||
<Exchange from="Orchestrator" to="H5 Audit" label="Certified final decision" status={goal.status} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-slate-100 bg-slate-50 px-5 py-3 text-[11px] leading-5 text-slate-500">Mỗi mũi tên thể hiện một boundary có kiểm soát; màu sắc lấy từ trạng thái thực tế của goal và từng worker.</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface MarkdownTextProps {
|
||||
text: string;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
function inlineMarkdown(value: string): ReactNode[] {
|
||||
const tokens = value.split(/(`[^`]+`|\*\*[^*]+\*\*|\[[^\]]+\]\(https?:\/\/[^)]+\))/g);
|
||||
return tokens.filter(Boolean).map((token, index) => {
|
||||
if (token.startsWith('`') && token.endsWith('`')) return <code key={index} className="rounded bg-slate-100 px-1.5 py-0.5 font-mono text-[0.9em] text-indigo-700">{token.slice(1, -1)}</code>;
|
||||
if (token.startsWith('**') && token.endsWith('**')) return <strong key={index} className="font-semibold text-slate-900">{token.slice(2, -2)}</strong>;
|
||||
const link = token.match(/^\[([^\]]+)\]\((https?:\/\/[^)]+)\)$/);
|
||||
if (link) return <a key={index} href={link[2]} target="_blank" rel="noreferrer" className="font-medium text-indigo-600 underline decoration-indigo-200 underline-offset-2 hover:text-indigo-800">{link[1]}</a>;
|
||||
return token;
|
||||
});
|
||||
}
|
||||
|
||||
export function MarkdownText({ text, compact = false }: MarkdownTextProps) {
|
||||
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
||||
const blocks: ReactNode[] = [];
|
||||
let list: string[] = [];
|
||||
let code: string[] = [];
|
||||
let inCode = false;
|
||||
|
||||
const flushList = () => {
|
||||
if (list.length === 0) return;
|
||||
blocks.push(<ul key={`list-${blocks.length}`} className="my-2 space-y-1 pl-5 text-sm leading-6 text-slate-700">{list.map((item, index) => <li key={index} className="list-disc marker:text-indigo-400">{inlineMarkdown(item)}</li>)}</ul>);
|
||||
list = [];
|
||||
};
|
||||
const flushCode = () => {
|
||||
if (code.length === 0) return;
|
||||
blocks.push(<pre key={`code-${blocks.length}`} className="my-3 overflow-x-auto rounded-xl bg-slate-950 p-4 text-xs leading-5 text-slate-100"><code>{code.join('\n')}</code></pre>);
|
||||
code = [];
|
||||
};
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (line.trim().startsWith('```')) {
|
||||
flushList();
|
||||
if (inCode) flushCode();
|
||||
inCode = !inCode;
|
||||
return;
|
||||
}
|
||||
if (inCode) { code.push(line); return; }
|
||||
const bullet = line.match(/^\s*[-*+]\s+(.+)$/);
|
||||
if (bullet) { list.push(bullet[1]); return; }
|
||||
flushList();
|
||||
const heading = line.match(/^(#{1,4})\s+(.+)$/);
|
||||
if (heading) {
|
||||
const size = heading[1].length === 1 ? 'text-lg' : heading[1].length === 2 ? 'text-base' : 'text-sm';
|
||||
blocks.push(<h3 key={`heading-${blocks.length}`} className={`mb-1 mt-3 font-semibold tracking-tight text-slate-900 ${size}`}>{inlineMarkdown(heading[2])}</h3>);
|
||||
} else if (/^---+$/.test(line.trim())) {
|
||||
blocks.push(<hr key={`rule-${blocks.length}`} className="my-3 border-slate-200" />);
|
||||
} else if (line.trim()) {
|
||||
blocks.push(<p key={`paragraph-${blocks.length}`} className="my-1 text-sm leading-6 text-slate-700">{inlineMarkdown(line)}</p>);
|
||||
} else if (!compact) {
|
||||
blocks.push(<div key={`space-${blocks.length}`} className="h-1" />);
|
||||
}
|
||||
});
|
||||
flushList();
|
||||
flushCode();
|
||||
|
||||
return <div className={compact ? 'line-clamp-3' : ''}>{blocks}</div>;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type KeyboardEvent, useMemo, useState } from 'react';
|
||||
import { type KeyboardEvent, useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { api, ChatAction, ChatAgent, ChatAnswer, ChatHistoryTurn, ChatModelProvider, ChatStreamPhase, SettingsActor } from '../lib/api';
|
||||
@@ -8,6 +8,17 @@ import { ModelConnectionPanel } from '../components/chat/ModelConnectionPanel';
|
||||
const ROLES = ['viewer', 'auditor', 'operator', 'project-admin', 'org-admin'];
|
||||
|
||||
type MessageKind = 'user' | 'assistant' | 'draft';
|
||||
type HarnessRunStatus = 'idle' | 'running' | 'completed' | 'failed';
|
||||
|
||||
const HARNESS_STAGES = [
|
||||
{ id: 'H1', label: 'Context' },
|
||||
{ id: 'H2', label: 'Tool' },
|
||||
{ id: 'H3', label: 'Evaluation' },
|
||||
{ id: 'H4', label: 'Security' },
|
||||
{ id: 'H5', label: 'Governance' },
|
||||
{ id: 'H6', label: 'AgentOps' },
|
||||
{ id: 'H7', label: 'Orchestration' },
|
||||
] as const;
|
||||
|
||||
interface WorkspaceMessage {
|
||||
id: string;
|
||||
@@ -95,6 +106,8 @@ export function Chat() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [streaming, setStreaming] = useState(true);
|
||||
const [streamBusy, setStreamBusy] = useState(false);
|
||||
const [harnessStatus, setHarnessStatus] = useState<HarnessRunStatus>('idle');
|
||||
const [activeHarness, setActiveHarness] = useState(0);
|
||||
|
||||
const auditQuery = useQuery({ queryKey: ['chat-audit'], queryFn: api.verifyChatAudit, retry: false });
|
||||
const conversationsQuery = useQuery({ queryKey: ['chat-history', actor], queryFn: () => api.chatHistory(actor), retry: false });
|
||||
@@ -142,16 +155,27 @@ export function Chat() {
|
||||
setPendingMessage(null);
|
||||
setError(null);
|
||||
setMessage('');
|
||||
setActiveHarness(HARNESS_STAGES.length - 1);
|
||||
setHarnessStatus('completed');
|
||||
refreshChat();
|
||||
},
|
||||
onError: (reason: unknown) => {
|
||||
setPendingMessage(null);
|
||||
setError(errorMessage(reason));
|
||||
setHarnessStatus('failed');
|
||||
},
|
||||
});
|
||||
|
||||
const busy = ask.isPending || streamBusy;
|
||||
|
||||
useEffect(() => {
|
||||
if (!busy || harnessStatus !== 'running') return undefined;
|
||||
const timer = window.setInterval(() => {
|
||||
setActiveHarness((current) => Math.min(current + 1, HARNESS_STAGES.length - 1));
|
||||
}, 1100);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [busy, harnessStatus]);
|
||||
|
||||
const runStream = async (text: string) => {
|
||||
setStreamBusy(true);
|
||||
setError(null);
|
||||
@@ -165,6 +189,8 @@ export function Chat() {
|
||||
setLast(finalToAnswer(phase, actor));
|
||||
setPendingMessage(null);
|
||||
setMessage('');
|
||||
setActiveHarness(HARNESS_STAGES.length - 1);
|
||||
setHarnessStatus('completed');
|
||||
}
|
||||
});
|
||||
refreshChat();
|
||||
@@ -172,6 +198,7 @@ export function Chat() {
|
||||
setPendingMessage(null);
|
||||
setDraftText(null);
|
||||
setError(errorMessage(reason));
|
||||
setHarnessStatus('failed');
|
||||
} finally {
|
||||
setStreamBusy(false);
|
||||
}
|
||||
@@ -180,6 +207,8 @@ export function Chat() {
|
||||
const submit = (override?: { message?: string; agentId?: string; skillId?: string }) => {
|
||||
const text = (override?.message ?? message).trim();
|
||||
if (!text || busy) return;
|
||||
setActiveHarness(0);
|
||||
setHarnessStatus('running');
|
||||
if (streaming && !override) void runStream(text);
|
||||
else {
|
||||
setPendingMessage(text);
|
||||
@@ -202,6 +231,8 @@ export function Chat() {
|
||||
setPendingMessage(null);
|
||||
setDraftText(null);
|
||||
setError(null);
|
||||
setHarnessStatus('idle');
|
||||
setActiveHarness(0);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -263,6 +294,34 @@ export function Chat() {
|
||||
</article>
|
||||
</div>
|
||||
))}
|
||||
{harnessStatus !== 'idle' && (
|
||||
<div aria-live="polite" aria-label="H1 to H7 response progress" className={`rounded-2xl border px-4 py-3 shadow-sm ${harnessStatus === 'failed' ? 'border-rose-200 bg-rose-50/80' : harnessStatus === 'completed' ? 'border-emerald-200 bg-emerald-50/70' : 'border-indigo-200 bg-indigo-50/70'}`}>
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-slate-800">
|
||||
<span className="relative flex h-2.5 w-2.5 items-center justify-center">
|
||||
{harnessStatus === 'running' && <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-indigo-400 opacity-60 motion-reduce:animate-none" />}
|
||||
<span className={`relative inline-flex h-2.5 w-2.5 rounded-full ${harnessStatus === 'failed' ? 'bg-rose-500' : harnessStatus === 'completed' ? 'bg-emerald-500' : 'bg-indigo-600'}`} />
|
||||
</span>
|
||||
{harnessStatus === 'running' ? `CASAN đang phản hồi · ${HARNESS_STAGES[activeHarness].id} ${HARNESS_STAGES[activeHarness].label}` : harnessStatus === 'completed' ? 'Phản hồi đã hoàn tất qua H1–H7' : `Phản hồi dừng tại ${HARNESS_STAGES[activeHarness].id}`}
|
||||
</div>
|
||||
<span className="text-[10px] font-bold uppercase tracking-[0.12em] text-slate-500">{harnessStatus === 'running' ? 'Processing' : harnessStatus}</span>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-7 gap-1.5">
|
||||
{HARNESS_STAGES.map((stage, index) => {
|
||||
const done = harnessStatus === 'completed' || index < activeHarness;
|
||||
const active = harnessStatus === 'running' && index === activeHarness;
|
||||
const failed = harnessStatus === 'failed' && index === activeHarness;
|
||||
return (
|
||||
<div key={stage.id} className="min-w-0 text-center">
|
||||
<div className={`h-1.5 rounded-full transition-colors duration-500 ${failed ? 'bg-rose-500' : done ? 'bg-emerald-500' : active ? 'animate-pulse bg-indigo-600 motion-reduce:animate-none' : 'bg-slate-200'}`} />
|
||||
<div className={`mt-1.5 text-[10px] font-bold ${failed ? 'text-rose-700' : done ? 'text-emerald-700' : active ? 'text-indigo-700' : 'text-slate-400'}`}>{stage.id}</div>
|
||||
<div className="hidden truncate text-[9px] text-slate-500 sm:block">{stage.label}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!historyQuery.isLoading && messages.length === 0 && <div className="mx-auto flex max-w-md flex-col items-center py-20 text-center"><div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-indigo-50 text-indigo-600"><Glyph name="spark" /></div><h3 className="mt-4 font-semibold text-slate-800">A governed empty state</h3><p className="mt-2 text-sm leading-6 text-slate-500">Ask for a plan, a security posture, or an evidence-backed comparison. CASAN will cite what it knows and decline what it cannot govern.</p></div>}
|
||||
</div>
|
||||
<div className="border-t border-slate-200 bg-white p-4">
|
||||
@@ -300,7 +359,7 @@ export function Chat() {
|
||||
</div>
|
||||
|
||||
<div className="mt-5 text-[10px] font-bold uppercase tracking-[0.15em] text-slate-400">Registered actions</div>
|
||||
<div className="mt-2 space-y-2">{(actionsQuery.data?.actions ?? []).slice(0, 3).map((action: ChatAction) => <button key={action.id} type="button" disabled={busy} onClick={() => { const trigger = action.triggers[0] || action.id; setMessage(trigger); setPendingMessage(trigger); ask.mutate({ message: trigger, agentId: 'ops-operator', skillId: 'registered-actions' }); }} className="w-full rounded-xl border border-slate-200 bg-white p-3 text-left transition hover:border-indigo-200 hover:bg-indigo-50/50 disabled:opacity-50"><div className="flex items-center gap-2 text-sm font-semibold text-slate-800"><Glyph name="bolt" />{action.label}</div><p className="mt-1 text-xs leading-5 text-slate-500">{action.description}</p></button>)}{actionsQuery.isError && <div className="text-xs text-rose-600">Registered actions are unavailable.</div>}</div>
|
||||
<div className="mt-2 space-y-2">{(actionsQuery.data?.actions ?? []).slice(0, 3).map((action: ChatAction) => <button key={action.id} type="button" disabled={busy} onClick={() => { const trigger = action.triggers[0] || action.id; setMessage(trigger); setPendingMessage(trigger); setActiveHarness(0); setHarnessStatus('running'); ask.mutate({ message: trigger, agentId: 'ops-operator', skillId: 'registered-actions' }); }} className="w-full rounded-xl border border-slate-200 bg-white p-3 text-left transition hover:border-indigo-200 hover:bg-indigo-50/50 disabled:opacity-50"><div className="flex items-center gap-2 text-sm font-semibold text-slate-800"><Glyph name="bolt" />{action.label}</div><p className="mt-1 text-xs leading-5 text-slate-500">{action.description}</p></button>)}{actionsQuery.isError && <div className="text-xs text-rose-600">Registered actions are unavailable.</div>}</div>
|
||||
</aside>
|
||||
</div>
|
||||
<ModelConnectionPanel actor={actor} open={connectionsOpen} onClose={() => setConnectionsOpen(false)} onSelect={(provider, model) => { setModelProvider(provider); setModelId(model); void connectionsQuery.refetch(); }} />
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import { api, type GoalJob, type SettingsActor } from '../lib/api';
|
||||
import { Card, StatusBadge } from '../components/ui/Card';
|
||||
import { TraceExplorer } from '../components/trace/TraceExplorer';
|
||||
import { MarkdownText } from '../components/ui/MarkdownText';
|
||||
import { ModelInteractionDiagram } from '../components/goals/ModelInteractionDiagram';
|
||||
|
||||
const DEFAULT_ACTOR: SettingsActor = { actor: 'local-operator', role: 'project-admin', project: 'default', tenant: 'default' };
|
||||
const TERMINAL = new Set<GoalJob['status']>(['completed', 'degraded', 'failed']);
|
||||
@@ -111,19 +113,21 @@ export function Goals() {
|
||||
<WorkerCard title="Cloud reviewer" subtitle="Independent critique and refinement" status={cloudStage?.status ?? 'queued'} detail={cloudStage?.detail ?? 'Waiting'} provider={cloudStage?.provider ?? selected.cloud_provider} model={cloudStage?.model ?? selected.cloud_model} />
|
||||
</div>
|
||||
|
||||
<ModelInteractionDiagram goal={selected} localStage={localStage} cloudStage={cloudStage} />
|
||||
|
||||
<Card title="Governed outcome" right={<StatusBadge value={selected.status} />}>
|
||||
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.12em] text-slate-400">Objective</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-700">{selected.goal}</p>
|
||||
<div className="mt-2"><MarkdownText text={selected.goal} /></div>
|
||||
</div>
|
||||
{selected.result ? <div className="mt-5 whitespace-pre-wrap text-sm leading-7 text-slate-800">{selected.result}</div> : (
|
||||
{selected.result ? <div className="mt-5"><MarkdownText text={selected.result} /></div> : (
|
||||
<div className="mt-5 flex items-center gap-3 rounded-xl border border-blue-200 bg-blue-50 p-4 text-sm text-blue-800">
|
||||
<span className="h-2.5 w-2.5 animate-pulse rounded-full bg-blue-500" />
|
||||
Models are working. This page refreshes automatically.
|
||||
</div>
|
||||
)}
|
||||
{selected.error && <div className="mt-4 rounded-xl border border-rose-200 bg-rose-50 p-3 text-sm text-rose-700">{selected.error}</div>}
|
||||
{selected.local_draft && <details className="mt-5 rounded-xl border border-slate-200 p-4"><summary className="cursor-pointer text-sm font-semibold text-slate-700">Inspect local worker draft</summary><div className="mt-4 whitespace-pre-wrap text-sm leading-6 text-slate-600">{selected.local_draft}</div></details>}
|
||||
{selected.local_draft && <details className="mt-5 rounded-xl border border-slate-200 p-4"><summary className="cursor-pointer text-sm font-semibold text-slate-700">Inspect local worker draft</summary><div className="mt-4"><MarkdownText text={selected.local_draft} /></div></details>}
|
||||
</Card>
|
||||
|
||||
<TraceExplorer traceId={selected.trace_id} />
|
||||
@@ -134,7 +138,7 @@ export function Goals() {
|
||||
<div className="space-y-2">
|
||||
{(listQuery.data?.goals ?? []).map((item) => (
|
||||
<button key={item.id} type="button" onClick={() => setSearchParams({ id: item.id })} className={`flex w-full items-center justify-between gap-4 rounded-xl border p-3 text-left transition hover:bg-slate-50 ${selectedId === item.id ? 'border-indigo-300 bg-indigo-50/50' : 'border-slate-200'}`}>
|
||||
<div className="min-w-0"><div className="truncate text-sm font-medium text-slate-800">{item.goal}</div><div className="mt-1 text-xs text-slate-400">{item.created_at.replace('T', ' ').replace('Z', '')}</div></div>
|
||||
<div className="min-w-0 flex-1"><MarkdownText text={item.goal} compact /><div className="mt-1 text-xs text-slate-400">{item.created_at.replace('T', ' ').replace('Z', '')}</div></div>
|
||||
<StatusBadge value={item.status} />
|
||||
</button>
|
||||
))}
|
||||
|
||||
@@ -117,11 +117,17 @@ def scan(text: str, mode: str):
|
||||
output = os.path.join(directory, "output.txt")
|
||||
with open(source, "w", encoding="utf-8") as handle:
|
||||
handle.write(text)
|
||||
environment = os.environ.copy()
|
||||
# The goal workflow already records and enforces its H4 boundary. Keep
|
||||
# deterministic injection/secret/PII checks active, but do not turn a
|
||||
# temporary semantic-classifier outage into a false-positive block.
|
||||
environment["CASAN_SECURITY_STRICT"] = "0"
|
||||
result = subprocess.run(
|
||||
["bash", SECURITY, source, output, mode],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=environment,
|
||||
timeout=60,
|
||||
)
|
||||
safe = ""
|
||||
@@ -141,6 +147,11 @@ def call_model(model: str, prompt: str, cloud: bool):
|
||||
handle.write(prompt)
|
||||
environment = os.environ.copy()
|
||||
environment["CASAN_PREFLIGHT"] = "1" if cloud else "0"
|
||||
# Long Markdown objectives need enough time for a local 9B model to
|
||||
# ingest the brief and produce a bounded plan. The outer goal timeout
|
||||
# remains the hard ceiling; this only raises the router's 60s default.
|
||||
environment.setdefault("CASAN_MODEL_TIMEOUT_SEC", os.environ.get("CASAN_GOAL_LOCAL_TIMEOUT_SEC", "240") if not cloud else "120")
|
||||
environment.setdefault("CASAN_MODEL_GENERATE_MAX_TOKENS", os.environ.get("CASAN_GOAL_MAX_OUTPUT_TOKENS", "1400"))
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["bash", MODEL_ROUTER, prompt_path, output_path, "--role", "generate", "--model", model],
|
||||
@@ -292,7 +303,7 @@ def run(job_path: str) -> int:
|
||||
if not ok:
|
||||
stage(job_path, "local-worker", "error", reason, job.get("local_provider", ""), local_model)
|
||||
emit(goal_id, "H2-tool", "error", "Local worker failed", {"reason": reason})
|
||||
raise RuntimeError("local_worker_failed")
|
||||
raise RuntimeError(f"local_worker_failed:{reason}")
|
||||
allowed, safe_local = scan(local_draft, "output")
|
||||
if not allowed:
|
||||
emit(goal_id, "H4-security", "blocked", "Local worker output rejected")
|
||||
|
||||
@@ -40,14 +40,26 @@ start_auth_bridge() {
|
||||
openssl rand -hex 32 > "$AUTH_BRIDGE_TOKEN_FILE"
|
||||
chmod 600 "$AUTH_BRIDGE_TOKEN_FILE"
|
||||
fi
|
||||
if [[ -f "$AUTH_BRIDGE_PID_FILE" ]] && kill -0 "$(cat "$AUTH_BRIDGE_PID_FILE")" 2>/dev/null; then
|
||||
if curl -fsS -m 2 "http://127.0.0.1:20130/healthz" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -f "$AUTH_BRIDGE_PID_FILE" ]]; then
|
||||
local existing_pid
|
||||
existing_pid="$(cat "$AUTH_BRIDGE_PID_FILE")"
|
||||
# A PID can be stale or reused by an unrelated process. Health is the
|
||||
# authoritative signal; clean the stale state before starting the bridge.
|
||||
kill "$existing_pid" 2>/dev/null || true
|
||||
rm -f "$AUTH_BRIDGE_PID_FILE"
|
||||
fi
|
||||
[[ -f "$AUTH_BRIDGE" ]] || { echo "CASAN_AUTH_BRIDGE_MISSING" >&2; return 1; }
|
||||
nohup python3 "$AUTH_BRIDGE" --bind 0.0.0.0 --port 20130 --token-file "$AUTH_BRIDGE_TOKEN_FILE" --audit-log "$AUTH_BRIDGE_AUDIT" > "$AUTH_BRIDGE_LOG" 2>&1 &
|
||||
echo "$!" > "$AUTH_BRIDGE_PID_FILE"
|
||||
chmod 600 "$AUTH_BRIDGE_PID_FILE" "$AUTH_BRIDGE_LOG" "$AUTH_BRIDGE_AUDIT" 2>/dev/null || true
|
||||
wait_url "http://127.0.0.1:20130/healthz"
|
||||
if ! wait_url "http://127.0.0.1:20130/healthz"; then
|
||||
kill "$(cat "$AUTH_BRIDGE_PID_FILE")" 2>/dev/null || true
|
||||
rm -f "$AUTH_BRIDGE_PID_FILE"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
stop_auth_bridge() {
|
||||
@@ -112,8 +124,8 @@ case "$CMD" in
|
||||
bash "$INFRA" status
|
||||
echo "=== control panel ==="
|
||||
cp_compose ps
|
||||
if [[ -f "$AUTH_BRIDGE_PID_FILE" ]] && kill -0 "$(cat "$AUTH_BRIDGE_PID_FILE")" 2>/dev/null; then
|
||||
echo "provider_auth_bridge=running pid=$(cat "$AUTH_BRIDGE_PID_FILE")"
|
||||
if curl -fsS -m 2 "http://127.0.0.1:20130/healthz" >/dev/null 2>&1; then
|
||||
echo "provider_auth_bridge=running"
|
||||
else
|
||||
echo "provider_auth_bridge=stopped"
|
||||
fi
|
||||
|
||||
@@ -171,6 +171,11 @@ def call_ollama(model_name, prompt, role):
|
||||
# small budget is consumed by reasoning and `response` comes back empty.
|
||||
body["think"] = False
|
||||
body["options"]["num_predict"] = 16 # terse final answer + fast
|
||||
else:
|
||||
# Prevent an unconstrained local generation from consuming the whole
|
||||
# request window. Goal Orchestrator can tune this without weakening the
|
||||
# shorter classifier/judge budgets.
|
||||
body["options"]["num_predict"] = max(64, int(os.environ.get("CASAN_MODEL_GENERATE_MAX_TOKENS", "1400")))
|
||||
data = json.dumps(body).encode()
|
||||
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
|
||||
t0 = time.time()
|
||||
|
||||
@@ -322,7 +322,7 @@ def runtime_env(provider_id: str, model_id: str) -> int:
|
||||
parsed = urlparse(endpoint)
|
||||
env.update({
|
||||
"CASAN_OLLAMA_HOST": parsed.netloc,
|
||||
"OLLAMA_HOST": parsed.netloc,
|
||||
"OLLAMA_HOST": endpoint,
|
||||
"CASAN_ALLOW_DOCKER_HOST_OLLAMA": "1" if parsed.hostname == "host.docker.internal" else "0",
|
||||
})
|
||||
print(json.dumps({"success": True, "env": env}, ensure_ascii=False))
|
||||
|
||||
@@ -28,6 +28,10 @@ mkdir -p "$(dirname "$PIN_FILE")"
|
||||
CMD="${1:-verify}"
|
||||
MODEL="${2:-${CASAN_MODEL:-ornith:9b}}"
|
||||
OLLAMA="${OLLAMA_HOST:-127.0.0.1:11434}"
|
||||
case "$OLLAMA" in
|
||||
http://*|https://*) OLLAMA_BASE="${OLLAMA%/}" ;;
|
||||
*) OLLAMA_BASE="http://${OLLAMA%/}" ;;
|
||||
esac
|
||||
|
||||
current_digest() {
|
||||
# 1) explicit override (deterministic for CI/tests) — DISABLED in enforced mode.
|
||||
@@ -44,7 +48,7 @@ current_digest() {
|
||||
fi
|
||||
# 2) live Ollama
|
||||
local d
|
||||
d="$(curl -sf "http://$OLLAMA/api/tags" 2>/dev/null | \
|
||||
d="$(curl -sf "$OLLAMA_BASE/api/tags" 2>/dev/null | \
|
||||
python3 -c "import json,sys
|
||||
m=sys.argv[1]
|
||||
try: d=json.load(sys.stdin)
|
||||
|
||||
Reference in New Issue
Block a user