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 3 commits
Commits
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
28 changes: 17 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,17 +214,23 @@ what it finds. Off by default. Worth knowing before touching it:
or deleting it there breaks the operator screen.
- **Its capability boundary is the allow-list** in `src/lib/operator/tool-scopes.ts`
— an allow-list, never a deny-list, because a deny-list silently grants any
endpoint the backend adds later. `WRITE_ENDPOINTS` holds 22 entries — read the
constant rather than this line, and read its doc comment before adding to it:
what is excluded (every `DELETE`, the full agent and group document PUTs, and
every `llmstore` write — each because that document carries an approval gate
of its own) is as deliberate as what is included.
Offering any of them is additionally gated by `isWriteScopeAvailable`
(backend HITL support, an already-verified gate, caller-identity auth, and a
mounted approval surface all have to hold — see `operator-activation.tsx`).
Granting `read_write` runs a write canary (`write-canary.ts`) that provokes
a real gated write and rolls the whole activation back on anything but a
clean pause.
endpoint the backend adds later. Read the constant rather than any count
quoted here, and read its doc comment before adding to it: what is excluded
(every `DELETE`, and the full agent and group document PUTs, each because that
document carries an approval gate of its own) is as deliberate as what is
included. `llmstore` writes ARE granted — that document can carry a gate
(`Task.toolApprovals` fully replaces the agent's), so the grant is valid only
alongside `gate-guard.ts`, which hard-refuses any llmstore write carrying that
field. Do not separate the two.
`read_write` is the DEFAULT scope and freely selectable on first activation
(read-only is the explicit opt-down); the safety lives in activation itself:
the gate is read back from the just-provisioned document
(`verifyGateInstalled`), and granting `read_write` runs a write canary
(`write-canary.ts`) that provokes a real gated write and rolls the whole
activation back on anything but a clean pause.
**Backend floor: operator activation requires EDDI 6.2.0+** — the allow-list
includes `GET /administration/docs{,/{name}}` (`@since 6.2.0`), and
`findMissingEndpoints` refuses activation when the spec lacks an entry.
- **Config is one atomic JSON blob** in the `platform.operator` global variable.
Activation writes several values that must land together and the variable
store has no transaction.
Expand Down
3 changes: 2 additions & 1 deletion HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@
- **Fixes along the way**: reconciling a resumed turn by placeholder identity (not output-count comparison) and handling a re-pause mid-resume; per-tool `toolApprovals.rules` verification, not just the top-level gate; the pause placeholder id moved from a ref into state (a ref update the resume handler read before React committed it raced the very state it was meant to protect).
- **`WRITE_ENDPOINTS` populated**: 22 entries, previously an intentionally empty allow-list stub. Four operational verbs (descriptor PATCH, deploy, undeploy, schedule disable), agent and group creation (`setup`, `setup-api`, `POST /groupstore/groups`), the workflow-extension authoring stores (rules/outputs/property-setters/dictionaries/apicalls/mcpcalls/workflows), and `PUT /agentstore/agents/{id}/updateResourceUri` — the hop that makes an edit actually take effect, since EDDI writes version+1 rather than mutating in place. **No `DELETE`, no full `PUT /agentstore/agents/{id}`, and no `llmstore` write** — that document carries a per-task `toolApprovals` which FULLY REPLACES the agent-level gate, so writing it could strip the operator's own oversight (which also means the operator cannot edit a prompt or model). `tool-scopes.ts` is the source of truth; its doc comment justifies every inclusion and exclusion.
- **Write canary**: `write-canary.ts` provokes one real gated write and asserts it actually pauses (rejecting the pause so nothing executes) before `read_write` is ever granted; anything but a clean pause rolls the whole activation back (`resetOperator` — undeploy + delete + clear config), since the agent is already deployed with live write tools by that point.
- **Real scope selection**: activation form's scope radio was previously pinned to `read_only`. `isWriteScopeAvailable` (`operator-activation.tsx`) requires backend HITL support, an already-verified gate, `authMode: "caller-identity"`, and a mounted approval surface to ALL hold before `read_write` is offered — which means it can only appear on a *reconfigure* of an already-verified operator, never a first activation (nothing to have verified yet). A `scope`/`effectiveScope` split keeps the UI from showing a choice as active that a lost precondition would silently not submit.
- **Real scope selection**: `read_write` is now the DEFAULT scope, freely selectable on first activation; read-only is the explicit opt-down. The old two-step bootstrap (activate read-only, reconfigure once the gate was verified) is gone — `isWriteScopeAvailable` only checks that `WRITE_ENDPOINTS` is non-empty, because activation itself proves the gate about the agent it actually creates: `verifyGateInstalled` reads the gate back from the new document, and the write canary refuses to leave a write-capable operator deployed unless a real write provably paused (rollback otherwise).
- **Backend floor**: operator activation now requires **EDDI 6.2.0+** unconditionally — `READ_ENDPOINTS` includes the docs endpoints (`GET /administration/docs{,/{name}}`, `@since 6.2.0`) and `findMissingEndpoints` hard-refuses activation when the spec lacks an entry. Accepted deliberately over an optional-endpoints tier (which would let the prompt promise docs the agent never got); the error message names the missing paths.
Comment thread
ginccc marked this conversation as resolved.
Resolved
- **Server-verified request preview**: the backend now resolves and fingerprints the exact HTTP request a gated call will make (EDDI-side: `IApiCallExecutor#resolve`, pinned at gate time, re-checked immediately before execution) and returns a redacted preview through `approval-status`. The operator screen's `RequestPreview` component renders that preview — method/uri/query/headers/body, with a `verified`/`preview` badge depending on whether the backend could pin it — demoting the old client-side `reconstructEndpoint` (`operationId` → guessed path) to a fallback for calls the backend could not resolve ahead of execution.
- **i18n**: `operator.readWriteChip`, `operator.activation.scope.*`, `operator.stage["write-canary"]`, `operator.toast.activatedReadWrite`, `operator.approval.{verified,verifiedTitle,previewOnly,previewOnlyTitle,query,headers,bodyTruncated}` — all 11 locales.
- **Backend companion PR** (`labsai/EDDI`, branch `feat/operator-request-fingerprint`): request-fingerprint pinning (`ResolvedRequest`, `RequestRedactor`), `eddi.operator.write.approval{decision}` metric plus the two `/administration/operator/{canary-result,gate-status}` relay endpoints this branch's canary and gate-verification calls report to.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "eddi-manager",
"private": true,
"version": "6.2.0",
"version": "6.3.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion src/components/editors/agent-config-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,7 @@ export const UserMemorySection = memo(function UserMemorySection({
</div>
<div>
<label className="mb-0.5 block text-[10px] text-muted-foreground">{t("agentDetail.dreamLlmModel", "LLM Model")}</label>
<DebouncedInput type="text" value={dream.llmModel ?? "claude-sonnet-4-6"} onCommit={(v) => patchDream({ llmModel: v })} placeholder="claude-sonnet-4-6" className="h-7 w-full rounded border border-input bg-background px-2 text-xs text-foreground font-mono focus:outline-none focus:ring-1 focus:ring-ring" />
<DebouncedInput type="text" value={dream.llmModel ?? "claude-sonnet-5"} onCommit={(v) => patchDream({ llmModel: v })} placeholder="claude-sonnet-5" className="h-7 w-full rounded border border-input bg-background px-2 text-xs text-foreground font-mono focus:outline-none focus:ring-1 focus:ring-ring" />
</div>
</div>
<div className="grid grid-cols-3 gap-3">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe("cascade editor — interactions drive the handlers", () => {

// Step 0 — model, confidence, timeout
const step0 = screen.getByTestId("cascade-step-0");
fireEvent.change(within(step0).getByPlaceholderText("e.g. claude-sonnet-4-6"), { target: { value: "gpt-4o-nano" } });
fireEvent.change(within(step0).getByPlaceholderText("e.g. claude-sonnet-5"), { target: { value: "gpt-4o-nano" } });
fireEvent.change(within(step0).getByPlaceholderText("empty = always accept"), { target: { value: "0.85" } });
fireEvent.change(within(step0).getByPlaceholderText("30000"), { target: { value: "8000" } });
// Provider select (first select inside the step card)
Expand Down
2 changes: 1 addition & 1 deletion src/components/editors/llm/cascade/cascade-step-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export function CascadeStepCard({
value={params.model ?? ""}
onChange={(e) => setParam("model", e.target.value)}
readOnly={readOnly}
placeholder={t("llmEditor.cascadeModelName", "e.g. claude-sonnet-4-6")}
placeholder={t("llmEditor.cascadeModelName", "e.g. claude-sonnet-5")}
className="h-7 flex-1 rounded border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
/>
{!readOnly && (
Expand Down
4 changes: 2 additions & 2 deletions src/components/editors/llm/task-memory-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function TaskMemorySection({ task, onChange, readOnly }: TaskSectionProps
...task.conversationSummary,
enabled: e.target.checked,
llmProvider: task.conversationSummary?.llmProvider ?? "anthropic",
llmModel: task.conversationSummary?.llmModel ?? "claude-sonnet-4-6",
llmModel: task.conversationSummary?.llmModel ?? "claude-sonnet-5",
maxSummaryTokens: task.conversationSummary?.maxSummaryTokens ?? 800,
recentWindowSteps: task.conversationSummary?.recentWindowSteps ?? 5,
maxRecallTurns: task.conversationSummary?.maxRecallTurns ?? 20,
Expand Down Expand Up @@ -151,7 +151,7 @@ export function TaskMemorySection({ task, onChange, readOnly }: TaskSectionProps
})
}
readOnly={readOnly}
placeholder="claude-sonnet-4-6"
placeholder="claude-sonnet-5"
className="h-8 w-full rounded-md border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
/>
</div>
Expand Down
2 changes: 0 additions & 2 deletions src/components/layout/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Outlet } from "react-router-dom";
import { Sidebar } from "./sidebar";
import { TopBar } from "./top-bar";
import { ChatDrawer } from "@/components/chat/chat-drawer";
import { OperatorDrawer } from "@/components/operator/operator-drawer";
import { WelcomeModal } from "@/components/onboarding/welcome-modal";
import { GuidedTour } from "@/components/onboarding/guided-tour";
import { TourOfferBar } from "@/components/onboarding/tour-offer-bar";
Expand Down Expand Up @@ -96,7 +95,6 @@ export function AppLayout() {
</main>
</div>
<ChatDrawer />
<OperatorDrawer />
<WelcomeModal />
<GuidedTour />
<TourOfferBar />
Expand Down
41 changes: 24 additions & 17 deletions src/components/layout/top-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useTheme } from "./theme-provider";
import { cn } from "@/lib/utils";
import { useAuth } from "@/hooks/use-auth";
import { PlatformStatus } from "./platform-status";
import { OperatorDrawer } from "@/components/operator/operator-drawer";

interface TopBarProps {
onMenuClick: () => void;
Expand Down Expand Up @@ -200,23 +201,28 @@ export function TopBar({ onMenuClick, sidebarVisible }: TopBarProps) {
{/* Center: Platform Status */}
<PlatformStatus />

{/* Right: Controls */}
<div className="flex items-center gap-2" data-tour="topbar-personalize">
{/* Language selector */}
<div className="relative flex items-center gap-1">
<Globe className="h-4 w-4 text-muted-foreground" />
<select
value={i18n.language}
onChange={(e) => i18n.changeLanguage(e.target.value)}
data-testid="language-selector"
className="appearance-none rounded-md bg-transparent px-2 py-1.5 text-sm text-foreground outline-none transition-colors hover:bg-secondary focus:ring-2 focus:ring-ring"
>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.label}
</option>
))}
</select>
{/* Right: Operator launcher, then the personalization controls. The
operator sits outside the tour target — the tour step is about theme
and language, not about the operator. */}
<div className="flex items-center gap-2">
<OperatorDrawer />

<div className="flex items-center gap-2" data-tour="topbar-personalize">
{/* Language selector */}
<div className="relative flex items-center gap-1">
<Globe className="h-4 w-4 text-muted-foreground" />
<select
value={i18n.language}
onChange={(e) => i18n.changeLanguage(e.target.value)}
data-testid="language-selector"
className="appearance-none rounded-md bg-transparent px-2 py-1.5 text-sm text-foreground outline-none transition-colors hover:bg-secondary focus:ring-2 focus:ring-ring"
>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.label}
</option>
))}
</select>
</div>

{/* Theme toggle */}
Expand Down Expand Up @@ -294,6 +300,7 @@ export function TopBar({ onMenuClick, sidebarVisible }: TopBarProps) {
)}
</div>
)}
</div>
</div>
</header>
);
Expand Down
Loading
Loading