Skip to content
This repository was archived by the owner on Sep 17, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fa793a0
feat(operator): build and verify the HITL approval gate (read-only)
ginccc Aug 1, 2026
1222379
feat(operator): the approval surface — resolve a pause inline in the …
ginccc Aug 1, 2026
f8e8098
fix(operator): verify per-tool rules too, and de-duplicate 3 locale l…
ginccc Aug 1, 2026
92e2d5d
fix(operator): reconcile the resumed turn by identity, and handle a r…
ginccc Aug 1, 2026
6b2fefc
feat(operator): derive the system prompt from the granted endpoint set
ginccc Aug 1, 2026
33cb961
fix(operator): close a gate-verification gap and two resume-path defects
ginccc Aug 1, 2026
829289d
fix(operator): keep the pause placeholder in state, not a ref
ginccc Aug 2, 2026
a1efbaf
feat(operator): populate WRITE_ENDPOINTS with the four curated writes
ginccc Aug 3, 2026
982e57d
feat(operator): the write canary — prove a real write pauses before g…
ginccc Aug 3, 2026
11f2615
feat(operator): let the admin actually choose read_write in the activ…
ginccc Aug 3, 2026
6f47e83
feat(operator): render the backend's resolved-request preview in the …
ginccc Aug 3, 2026
1d022dd
docs: catch up HANDOFF and AGENTS on the write-scope arc
ginccc Aug 3, 2026
f729b98
feat(operator): let the operator author agent groups, and flag capabi…
ginccc Aug 3, 2026
f93a047
fix(operator): two review findings on the write path
ginccc Aug 3, 2026
74f5188
fix(operator): four review findings on the activation and canary paths
ginccc Aug 3, 2026
d71a0aa
docs(operator): record why an absent hitlPausedAt is not treated as a…
ginccc Aug 3, 2026
864afd3
docs(operator): drop a stale endpoint count from a test comment
ginccc Aug 3, 2026
a5fea34
docs(operator): correct the AbortSignal.any rationale
ginccc Aug 3, 2026
addf66b
fix(hitl): source pause reason and timeouts from approval-status
ginccc Aug 3, 2026
14040d3
feat(hitl): let the approvals inbox decide TOOL_CALL pauses in place
ginccc Aug 3, 2026
c306cdb
feat(operator): create and modify agents and groups, not just deploy …
ginccc Aug 3, 2026
e22850c
fix(operator): close a self-ungating path, a dead canary, and two lie…
ginccc Aug 3, 2026
8dead9a
feat(operator): complete the authoring cascade, and refuse a self-aim…
ginccc Aug 3, 2026
d25ddc8
fix(operator): close four escalation-flag evasions, and a hole in gat…
ginccc Aug 3, 2026
691cb8e
fix(operator): the self-guard was inert for approvers, and missed a t…
ginccc Aug 3, 2026
eb35fe0
fix(operator): malformed hitlConfig JSON took down the approval surface
ginccc Aug 3, 2026
ffa87b9
fix(operator): roll back a half-provisioned activation, and stop hidi…
ginccc Aug 4, 2026
578c458
feat(operator): a context-aware side-chat drawer, in Manager and Work…
ginccc Aug 4, 2026
a6ab700
fix(operator): six defects from the final review, one of which broke …
ginccc Aug 4, 2026
79fd7fe
feat(operator): show what a whole-document config write CHANGES
ginccc Aug 4, 2026
ee9d176
feat(operator): keyboard-close the drawer, and say when a decision is…
ginccc Aug 4, 2026
b0fb907
fix(hitl): a failed pause-details read left Approve enabled, not blocked
ginccc Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion src/components/operator/operator-status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,24 @@ import {
RefreshCw,
Trash2,
PlugZap,
ShieldCheck,
ShieldAlert,
ShieldQuestion,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AlertDialog } from "@/components/ui/alert-dialog";
import type { OperatorConfig } from "@/lib/api/operator";
import type { OperatorConfig, GateVerificationResult } from "@/lib/api/operator";
import type { DeploymentStatus } from "@/lib/api/agents";

interface OperatorStatusPanelProps {
config: OperatorConfig;
status: DeploymentStatus | null | undefined;
statusLoading: boolean;
/** Result of re-reading the approval gate off the live agent document. */
gate: GateVerificationResult | null | undefined;
gateLoading: boolean;
onReconfigure: () => void;
onDeactivate: () => void;
onReset: () => void;
Expand All @@ -35,6 +41,8 @@ export function OperatorStatusPanel({
config,
status,
statusLoading,
gate,
gateLoading,
onReconfigure,
onDeactivate,
onReset,
Expand Down Expand Up @@ -63,6 +71,7 @@ export function OperatorStatusPanel({
<Lock className="h-3 w-3" />
{t("operator.readOnlyChip", "Read-only")}
</Badge>
<GateBadge gate={gate} loading={gateLoading} />
</div>

<div className="space-y-1 text-sm">
Expand All @@ -89,6 +98,22 @@ export function OperatorStatusPanel({
</div>
)}

{/* Nothing depends on this today — read_only never offers a write tool
regardless — but it is checked and shown on every load so the fact is
never assumed the one time it starts to matter (read_write scope). */}
{gate && !gate.verified && (
<div
className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive"
data-testid="operator-gate-unverified"
>
<ShieldAlert className="mt-0.5 h-4 w-4 shrink-0" />
<span>
{t("operator.status.gateUnverifiedHelp", "The approval gate could not be verified on this agent's document.")}
{gate.reason ? ` (${gate.reason})` : ""}
</span>
</div>
)}

<div className="space-y-2 pt-2">
<Button
variant="outline"
Expand Down Expand Up @@ -166,6 +191,52 @@ export function OperatorStatusPanel({
);
}

/**
* Shows whether the tool-approval gate is currently verified on the live agent
* document. Independent of the deployment-status badge above: a `READY`
* deployment says nothing about whether `hitlConfig` survived version skew or
* was flipped off deployment-wide — this badge is the one thing that does.
*/
function GateBadge({
gate,
loading,
}: {
gate: import("@/lib/api/operator").GateVerificationResult | null | undefined;
loading: boolean;
}) {
const { t } = useTranslation();
if (loading && !gate) {
return (
<Badge variant="secondary" className="gap-1" data-testid="operator-gate-checking">
<Loader2 className="h-3 w-3 animate-spin" />
{t("operator.status.gateChecking", "Verifying gate…")}
</Badge>
);
}
if (!gate) {
return (
<Badge variant="outline" className="gap-1" data-testid="operator-gate-unknown">
<ShieldQuestion className="h-3 w-3" />
{t("operator.status.gateUnknown", "Gate not checked")}
</Badge>
);
}
if (gate.verified) {
return (
<Badge variant="success" className="gap-1" data-testid="operator-gate-verified">
<ShieldCheck className="h-3 w-3" />
{t("operator.status.gateVerified", "Gate verified")}
</Badge>
);
}
return (
<Badge variant="destructive" className="gap-1" data-testid="operator-gate-badge-unverified">
<ShieldAlert className="h-3 w-3" />
{t("operator.status.gateNotVerified", "Gate not verified")}
</Badge>
);
}

function Row({ label, value }: { label: string; value: string }) {
return (
<div className="flex justify-between gap-3">
Expand Down
40 changes: 38 additions & 2 deletions src/hooks/use-operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
fetchOpenApiSpec,
findMissingEndpoints,
defaultOperatorConfig,
verifyGateInstalled,
type GateVerificationResult,
type OperatorConfig,
} from "@/lib/api/operator";
import { undeployAgent, deleteAgent } from "@/lib/api/agents";
Expand All @@ -27,6 +29,7 @@ export const operatorKeys = {
config: ["operator", "config"] as const,
status: (agentId: string, version: number) =>
["operator", "status", agentId, version] as const,
gate: (agentId: string) => ["operator", "gate", agentId] as const,
};

/* ─── Config ─── */
Expand Down Expand Up @@ -68,13 +71,15 @@ export type ActivationStage =
| "provisioning"
| "resolving-version"
| "saving"
| "verifying-gate"
| "canary"
| "done";

/** What activation returns: the saved config plus the probe outcome. */
/** What activation returns: the saved config plus the probe outcomes. */
export interface ActivationOutcome {
config: OperatorConfig;
canary: CanaryResult;
gate: GateVerificationResult;
}

export interface ActivateParams {
Expand Down Expand Up @@ -142,13 +147,22 @@ export function useActivateOperator() {
}
}

// Read the gate back from the document we just created — never trust that
// sending hitlConfig means it landed. This is what actually proves
// isWriteScopeAvailable's "backend accepts hitlConfig" fact; a caught
// exception here still resolves (not throws) so a read_only activation
// that failed only its verification step is reported, not treated as a
// failed provision.
onStage?.("verifying-gate");
const gate = await verifyGateInstalled(result.agentId);

// A READY badge only proves the config loaded. Run one real read so a
// deployed-but-unreachable operator is reported as such, not as success.
onStage?.("canary");
const canary = await runOperatorCanary(next);

onStage?.("done");
return { config: next, canary };
return { config: next, canary, gate };
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: operatorKeys.all });
Expand Down Expand Up @@ -191,6 +205,28 @@ export function useOperatorCanary() {
});
}

/**
* Continuously re-verifies the tool-approval gate on the live agent document.
*
* Verification is continuous, not one-time: `eddi.hitl.tool.enabled=false` (or
* any other deployment-level change) can be flipped after activation and
* nothing else would report it. `staleTime: 0` makes every mount — i.e. every
* time the operator page is opened — re-check rather than serve a cached
* "verified" from a stale mount. A failed or inconclusive result must drop any
* write-scope offer; this hook only reports the fact, callers are responsible
* for failing closed on it (see `isWriteScopeAvailable`).
*/
export function useVerifyOperatorGate(config: OperatorConfig | null | undefined) {
const agentId = config?.agentId ?? "";
const ready = Boolean(config?.enabled && agentId);
return useQuery({
queryKey: operatorKeys.gate(agentId),
queryFn: () => verifyGateInstalled(agentId),
enabled: ready,
staleTime: 0,
});
}

/* ─── Kill switch ─── */

/** Undeploy the operator and mark it disabled. Reversible by re-activating. */
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -3137,6 +3137,7 @@
"provisioning": "يجري إنشاء وكيل المُشغّل…",
"resolving-version": "يجري تحديد إصدار الوكيل…",
"saving": "يجري حفظ الإعدادات…",
"verifying-gate": "جارٍ التحقق من بوابة الموافقة…",
"done": "تم",
"canary": "يجري التحقق من أن المُشغّل يصل إلى منصتك…"
},
Expand All @@ -3159,7 +3160,12 @@
"deactivateConfirmBody": "سيُزال المُشغّل من النشر ويتوقف عن الاستجابة. تُحفظ إعداداته حتى تتمكن من تفعيله لاحقًا.",
"resetConfirmTitle": "حذف مُشغّل المنصة؟",
"resetConfirmBody": "سيُحذف وكيل المُشغّل وجميع الموارد التي أُنشئت له نهائيًا، إضافةً إلى إعداداته المحفوظة. لا يمكن التراجع عن هذا الإجراء.",
"connectionCheck": "فحص الاتصال"
"connectionCheck": "فحص الاتصال",
"gateChecking": "جارٍ التحقق من البوابة…",
"gateUnknown": "لم يتم فحص البوابة",
"gateVerified": "تم التحقق من البوابة",
"gateNotVerified": "لم يتم التحقق من البوابة",
"gateUnverifiedHelp": "تعذّر التحقق من بوابة الموافقة في مستند هذا الوكيل."
},
"chat": {
"empty": "اسأل عن نشرك. يبحث المُشغّل عن الإجابات ويعرض لك كل استدعاء يقوم به.",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -3113,6 +3113,7 @@
"provisioning": "Operator-Agent wird erstellt …",
"resolving-version": "Agent-Version wird ermittelt …",
"saving": "Konfiguration wird gespeichert …",
"verifying-gate": "Die Freigabesperre wird überprüft…",
"done": "Fertig",
"canary": "Es wird geprüft, ob der Operator Ihre Plattform erreicht …"
},
Expand All @@ -3135,7 +3136,12 @@
"deactivateConfirmBody": "Der Operator wird undeployt und antwortet nicht mehr. Seine Konfiguration bleibt erhalten, sodass Sie ihn später wieder aktivieren können.",
"resetConfirmTitle": "Plattform-Operator löschen?",
"resetConfirmBody": "Der Operator-Agent und alle für ihn erstellten Ressourcen werden dauerhaft gelöscht, ebenso seine gespeicherte Konfiguration. Dies kann nicht rückgängig gemacht werden.",
"connectionCheck": "Verbindung prüfen"
"connectionCheck": "Verbindung prüfen",
"gateChecking": "Sperre wird überprüft…",
"gateUnknown": "Sperre nicht geprüft",
"gateVerified": "Sperre verifiziert",
"gateNotVerified": "Sperre nicht verifiziert",
"gateUnverifiedHelp": "Die Freigabesperre konnte im Dokument dieses Agenten nicht verifiziert werden."
},
"chat": {
"empty": "Fragen Sie nach Ihrer Installation. Der Operator schlägt nach und zeigt Ihnen jeden Aufruf, den er macht.",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3140,6 +3140,7 @@
"provisioning": "Creating the operator agent…",
"resolving-version": "Resolving the agent version…",
"saving": "Saving configuration…",
"verifying-gate": "Verifying the approval gate…",
"done": "Done",
"canary": "Checking the operator can reach your platform…"
},
Expand All @@ -3162,7 +3163,12 @@
"deactivateConfirmBody": "The operator is undeployed and stops responding. Its configuration is kept, so you can turn it back on later.",
"resetConfirmTitle": "Delete the Platform Operator?",
"resetConfirmBody": "The operator agent and all the resources created for it are permanently deleted, along with its saved configuration. This cannot be undone.",
"connectionCheck": "Check connection"
"connectionCheck": "Check connection",
"gateChecking": "Verifying gate…",
"gateUnknown": "Gate not checked",
"gateVerified": "Gate verified",
"gateNotVerified": "Gate not verified",
"gateUnverifiedHelp": "The approval gate could not be verified on this agent's document."
},
"chat": {
"empty": "Ask about your deployment. The operator looks things up and shows you every call it makes.",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -3119,6 +3119,7 @@
"provisioning": "Creando el agente operador…",
"resolving-version": "Resolviendo la versión del agente…",
"saving": "Guardando la configuración…",
"verifying-gate": "Verificando la puerta de aprobación…",
"done": "Listo",
"canary": "Comprobando que el operador puede acceder a tu plataforma…"
},
Expand All @@ -3141,7 +3142,12 @@
"deactivateConfirmBody": "El operador se retira del despliegue y deja de responder. Su configuración se conserva, así que puedes volver a activarlo más adelante.",
"resetConfirmTitle": "¿Eliminar el operador de plataforma?",
"resetConfirmBody": "El agente operador y todos los recursos creados para él se eliminan de forma permanente, junto con su configuración guardada. Esta acción no se puede deshacer.",
"connectionCheck": "Comprobar conexión"
"connectionCheck": "Comprobar conexión",
"gateChecking": "Verificando puerta…",
"gateUnknown": "Puerta no comprobada",
"gateVerified": "Puerta verificada",
"gateNotVerified": "Puerta no verificada",
"gateUnverifiedHelp": "No se pudo verificar la puerta de aprobación en el documento de este agente."
},
"chat": {
"empty": "Pregunta sobre tu instalación. El operador lo consulta y te muestra cada llamada que hace.",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -3119,6 +3119,7 @@
"provisioning": "Création de l'agent opérateur…",
"resolving-version": "Résolution de la version de l'agent…",
"saving": "Enregistrement de la configuration…",
"verifying-gate": "Vérification de la barrière d'approbation…",
"done": "Terminé",
"canary": "Vérification que l'opérateur atteint votre plateforme…"
},
Expand All @@ -3141,7 +3142,12 @@
"deactivateConfirmBody": "L'opérateur est retiré du déploiement et cesse de répondre. Sa configuration est conservée, vous pourrez le réactiver plus tard.",
"resetConfirmTitle": "Supprimer l'opérateur de plateforme ?",
"resetConfirmBody": "L'agent opérateur et toutes les ressources créées pour lui sont définitivement supprimés, ainsi que sa configuration enregistrée. Cette action est irréversible.",
"connectionCheck": "Vérifier la connexion"
"connectionCheck": "Vérifier la connexion",
"gateChecking": "Vérification de la barrière…",
"gateUnknown": "Barrière non vérifiée",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
"gateVerified": "Barrière vérifiée",
"gateNotVerified": "Barrière non vérifiée",
"gateUnverifiedHelp": "La barrière d'approbation n'a pas pu être vérifiée sur le document de cet agent."
},
"chat": {
"empty": "Interrogez votre déploiement. L'opérateur fait les recherches et vous montre chaque appel qu'il effectue.",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -3113,6 +3113,7 @@
"provisioning": "ऑपरेटर एजेंट बनाया जा रहा है…",
"resolving-version": "एजेंट का संस्करण निर्धारित किया जा रहा है…",
"saving": "कॉन्फ़िगरेशन सहेजा जा रहा है…",
"verifying-gate": "अनुमोदन गेट सत्यापित किया जा रहा है…",
"done": "हो गया",
"canary": "जाँचा जा रहा है कि ऑपरेटर आपके प्लेटफ़ॉर्म तक पहुँच सकता है…"
},
Expand All @@ -3135,7 +3136,12 @@
"deactivateConfirmBody": "ऑपरेटर का परिनियोजन हटा दिया जाता है और वह उत्तर देना बंद कर देता है। इसका कॉन्फ़िगरेशन सुरक्षित रहता है, ताकि आप इसे बाद में फिर चालू कर सकें।",
"resetConfirmTitle": "प्लेटफ़ॉर्म ऑपरेटर हटाएँ?",
"resetConfirmBody": "ऑपरेटर एजेंट और उसके लिए बनाए गए सभी संसाधन, साथ ही सहेजा गया कॉन्फ़िगरेशन, स्थायी रूप से हटा दिए जाते हैं। इसे पूर्ववत नहीं किया जा सकता।",
"connectionCheck": "कनेक्शन जाँचें"
"connectionCheck": "कनेक्शन जाँचें",
"gateChecking": "गेट सत्यापित हो रहा है…",
"gateUnknown": "गेट जाँचा नहीं गया",
"gateVerified": "गेट सत्यापित",
"gateNotVerified": "गेट सत्यापित नहीं",
"gateUnverifiedHelp": "इस एजेंट के दस्तावेज़ पर अनुमोदन गेट सत्यापित नहीं किया जा सका।"
},
"chat": {
"empty": "अपने परिनियोजन के बारे में पूछें। ऑपरेटर जानकारी खोजता है और अपनी हर कॉल आपको दिखाता है।",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -3113,6 +3113,7 @@
"provisioning": "オペレーターエージェントを作成しています…",
"resolving-version": "エージェントのバージョンを確認しています…",
"saving": "設定を保存しています…",
"verifying-gate": "承認ゲートを確認しています…",
"done": "完了",
"canary": "オペレーターが環境に到達できるか確認しています…"
},
Expand All @@ -3135,7 +3136,12 @@
"deactivateConfirmBody": "オペレーターはデプロイ解除され、応答を停止します。設定は保持されるため、後で再度有効にできます。",
"resetConfirmTitle": "プラットフォームオペレーターを削除しますか?",
"resetConfirmBody": "オペレーターエージェントと、そのために作成されたすべてのリソース、保存された設定が完全に削除されます。この操作は取り消せません。",
"connectionCheck": "接続を確認"
"connectionCheck": "接続を確認",
"gateChecking": "ゲートを確認中…",
"gateUnknown": "ゲート未確認",
"gateVerified": "ゲート確認済み",
"gateNotVerified": "ゲート未確認",
"gateUnverifiedHelp": "このエージェントのドキュメントで承認ゲートを確認できませんでした。"
},
"chat": {
"empty": "ご自身の環境について質問してください。オペレーターが調べ、行ったすべての呼び出しを表示します。",
Expand Down
8 changes: 7 additions & 1 deletion src/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -3113,6 +3113,7 @@
"provisioning": "오퍼레이터 에이전트를 만드는 중…",
"resolving-version": "에이전트 버전을 확인하는 중…",
"saving": "설정을 저장하는 중…",
"verifying-gate": "승인 게이트를 확인하는 중…",
"done": "완료",
"canary": "오퍼레이터가 플랫폼에 접근할 수 있는지 확인하는 중…"
},
Expand All @@ -3135,7 +3136,12 @@
"deactivateConfirmBody": "오퍼레이터의 배포가 해제되고 응답을 멈춥니다. 설정은 유지되므로 나중에 다시 켤 수 있습니다.",
"resetConfirmTitle": "플랫폼 오퍼레이터를 삭제할까요?",
"resetConfirmBody": "오퍼레이터 에이전트와 이를 위해 생성된 모든 리소스가 저장된 설정과 함께 영구히 삭제됩니다. 되돌릴 수 없습니다.",
"connectionCheck": "연결 확인"
"connectionCheck": "연결 확인",
"gateChecking": "게이트 확인 중…",
"gateUnknown": "게이트 미확인",
"gateVerified": "게이트 확인됨",
"gateNotVerified": "게이트 확인 안 됨",
"gateUnverifiedHelp": "이 에이전트 문서에서 승인 게이트를 확인할 수 없습니다."
},
"chat": {
"empty": "배포에 대해 물어보세요. 오퍼레이터가 찾아보고, 수행한 모든 호출을 보여줍니다.",
Expand Down
Loading
Loading