feat(operator): write by default, editable LLM configs behind a gate guard, docs access, and a header launcher - #137
Conversation
…hells' headers The bottom-right position was collision avoidance, not design. That corner already holds sonner's toast viewport (z-999999999, which covered the launcher outright whenever a toast was up), ChatDrawer's composer, WorkforceBottomTabs, and workforce-dashboard's own MobileFab — the last of which won the hit test, so tapping the operator navigated to /workforce/new. Every collision bought a hardcoded offset (bottom-24, then bottom-40) that held only until the next thing landed in that corner. It also had to be mounted four times, because AppLayout and WorkforceLayout's three viewport branches share no chrome slot. The header is that slot: TopBar and WorkforceTopbar between them cover all four. The drawer is now an inline icon button plus a panel anchored to it (absolute end-0 top-full), so no offset is hardcoded at all. Tab order improves as a side effect — the panel now follows its launcher in the DOM instead of preceding it. Pending-approval dot, role gating, Escape-to-close, focus restore and the shared conversation store are unchanged. The test that pinned bottom-24/bottom-40 is replaced by one asserting the panel positions relative to its launcher and reintroduces no viewport-fixed offset.
…guard, and docs access
Three changes that together make the Platform Operator actually able to
operate the platform, plus the model-default sweep they surfaced.
WRITE BY DEFAULT. read_write is now the default scope and freely selectable
on first activation; read-only is the explicit opt-down. The old two-step
bootstrap (activate read-only, come back, reconfigure once a gate was
verified) gated the OFFER on facts activation now proves 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. That is strictly stronger
evidence than a verification remembered from a predecessor agent, so
isWriteScopeAvailable now only checks that WRITE_ENDPOINTS is non-empty.
Caller-identity is no longer demanded either: on an OIDC deployment
authMode 'none' cannot activate at all, and on a no-auth deployment there
are no identities to attribute approvals to — requiring it made write scope
permanently unreachable exactly where EDDI gets evaluated.
LLM CONFIGS, SAFELY. llmstore writes are granted, so the operator can edit
an agent's system prompt and model — previously impossible and the single
biggest capability gap. That document can carry a gate
(Task.toolApprovals fully replaces the agent-level one), so the grant is
valid ONLY alongside the new gate-guard.ts, which hard-refuses (disables
Approve, like self-guard.ts) any llmstore write carrying that field, and
any whose body cannot be read in full to prove it does not. Absence is safe
rather than merely unchecked: a body without toolApprovals leaves the field
null, so the agent-level gate applies — the operator can change what an
agent says and runs, never what gates it. blocked-calls.ts now resolves
both guards in one place, because the three approval surfaces were each
inlining self-guard plus a copy of its i18n string and a second guard would
have triplicated that again.
DOCS. GET /administration/docs{,/{name}} were built by the backend (EDDI
6.2.0) precisely so an OpenAPI-generated agent picks them up as ordinary
tools — the eddi://docs/* MCP resources do not reach an agent. The
allow-list simply never included them. Adding them sets a hard EDDI 6.2.0+
floor for activation, accepted deliberately over an optional-endpoints tier
that would let the prompt promise docs the agent never got. The system
prompt also gains the versioning model (agent -> workflow -> config by id
and version, nothing edits in place) in BOTH scopes, since 'my change did
nothing' is a read-only diagnosis question.
MODEL DEFAULTS. claude-sonnet-5 is now the default and placeholder
everywhere — the anthropic provider default fed the agent wizard, group
wizard, operator activation and Workforce team builder, and every
hardcoded copy is updated (four group-wizard fallbacks, cascade, task
memory, dream loop, and cascadeModelName across all 11 locales). Bedrock's
stale anthropic.claude-sonnet-4-6-v1:0 becomes anthropic.claude-sonnet-5.
Stale copy fixed with it: the dashboard discovery card and the operator
empty state both still described a read-only operator.
…handoff docs Version drives the sidebar badge via __APP_VERSION__ (vite define from package.json). AGENTS.md and HANDOFF.md both still described the pre-change operator: read-only by default, llmstore permanently excluded, and no backend floor. All three are now stated as they actually are, including that the llmstore grant is valid only alongside gate-guard.ts and that the two must not be separated.
|
Warning Review limit reached
Next review available in: 58 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (46)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Makes the Platform Operator write-capable by default, expands its tools, and moves its launcher into shared headers.
Changes:
- Adds gated LLM configuration writes and documentation access.
- Defaults activation to read/write and centralizes approval blocking.
- Standardizes Claude Sonnet 5 defaults and relocates the operator launcher.
Reviewed changes
Copilot reviewed 45 out of 45 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/pages/operator.tsx |
Uses centralized approval guards and updated copy. |
src/pages/group-wizard.tsx |
Updates model fallbacks. |
src/pages/dashboard.tsx |
Updates operator discovery copy. |
src/pages/conversation-detail.tsx |
Uses centralized approval guards. |
src/pages/approvals.tsx |
Uses centralized approval guards. |
src/pages/__tests__/operator.test.tsx |
Updates scope and model expectations. |
src/lib/operator/tool-scopes.ts |
Expands tools and defaults write availability. |
src/lib/operator/system-prompt.ts |
Adds docs, architecture, and LLM-edit guidance. |
src/lib/operator/gate-guard.ts |
Detects gate-carrying LLM writes. |
src/lib/operator/blocked-calls.ts |
Combines hard approval refusals. |
src/lib/operator/__tests__/tool-scopes.test.ts |
Updates allow-list tests. |
src/lib/operator/__tests__/system-prompt.test.ts |
Tests new prompt guidance. |
src/lib/operator/__tests__/gate-guard.test.ts |
Tests the new gate guard. |
src/lib/model-suggestions.ts |
Adds current Claude model suggestions. |
src/lib/api/operator.ts |
Changes operator defaults. |
src/lib/api/agent-setup.ts |
Changes provider model defaults. |
src/lib/api/__tests__/operator.test.ts |
Tests default write provisioning. |
src/i18n/locales/zh.json |
Updates Chinese strings. |
src/i18n/locales/th.json |
Updates Thai strings. |
src/i18n/locales/pt.json |
Updates Portuguese strings. |
src/i18n/locales/ko.json |
Updates Korean strings. |
src/i18n/locales/ja.json |
Updates Japanese strings. |
src/i18n/locales/hi.json |
Updates Hindi strings. |
src/i18n/locales/fr.json |
Updates French strings. |
src/i18n/locales/es.json |
Updates Spanish strings. |
src/i18n/locales/en.json |
Updates English strings. |
src/i18n/locales/de.json |
Updates German strings. |
src/i18n/locales/ar.json |
Updates Arabic strings. |
src/hooks/use-operator.ts |
Reframes gate verification as monitoring. |
src/components/workforce/workforce-topbar.tsx |
Adds the operator launcher. |
src/components/workforce/workforce-layout.tsx |
Removes floating launcher mounts. |
src/components/workforce/wizard/team-builder.tsx |
Updates model placeholder. |
src/components/operator/operator-drawer.tsx |
Converts the FAB into a header panel. |
src/components/operator/operator-activation.tsx |
Defaults scope selection to read/write. |
src/components/operator/__tests__/operator-drawer.test.tsx |
Tests header-relative positioning. |
src/components/operator/__tests__/operator-activation.test.tsx |
Tests write-first activation. |
src/components/layout/top-bar.tsx |
Adds the operator launcher. |
src/components/layout/app-layout.tsx |
Removes the floating launcher. |
src/components/editors/llm/task-memory-section.tsx |
Updates memory model defaults. |
src/components/editors/llm/cascade/cascade-step-card.tsx |
Updates cascade placeholder. |
src/components/editors/llm/cascade/__tests__/cascade-interactions.test.tsx |
Updates placeholder expectation. |
src/components/editors/agent-config-sections.tsx |
Updates dream-loop model default. |
package.json |
Bumps the application version. |
HANDOFF.md |
Documents scope and backend-floor changes. |
AGENTS.md |
Documents operator safety architecture. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ned preview Addresses CodeRabbit's review on #137. Two of the five were real security defects, and the first one invalidated this PR's core justification. ENFORCE THE GATE READ-BACK. isWriteScopeAvailable stopped demanding a gate verified on a PREVIOUS operator, on the stated grounds that activation proves the gate about the agent it actually creates. But verifyGateInstalled was only REPORTED — reportOperatorGateStatus, then proceed — so that justification was false as written, and a read_write activation could leave a deployed operator whose gate never verified. The write canary is not a substitute: it provokes ONE endpoint (a descriptor PATCH), so it proves the patch pattern pauses and says nothing about http.post/put/delete; a document gating only some methods passes the canary with the rest ungated. Now enforced for read_write (roll back, then throw with the reason gateLooksInstalled gave) and deliberately still advisory for read_only, where an unverified operator is useless rather than dangerous. DO NOT TRUST AN UNPINNED PREVIEW. gate-guard read requestPreview.body without checking requestPinned. Per PendingToolCallView's own contract an unpinned http call is previewed best-effort and 'can still change before it runs' — so a body inspected and found gate-free can execute carrying toolApprovals. That is a bypass of the control, not a gap in it. Unpinned llmstore writes are now refused, same fail-closed rule as a truncated body. FALSE DIAGNOSIS. unverifiable-body covers truncation AND parse failure, but the message named only size, so a small malformed payload was blocked with a wrong explanation. Each refusal mode now has its own accurate string; blockedGateUnpinned added across all 11 locales. TWO STALE DOCS. tool-scopes' 'Deliberately NOT here' still listed every llmstore write as excluded, contradicting the grant a few lines above in the same load-bearing comment. HANDOFF still said no llmstore write and that prompts/models cannot be edited. Both corrected, and both now state that the grant and gate-guard must not be separated. Tests: new use-operator-activation.test.tsx (4) covering the enforcement, mutation-checked — disabling the enforcement fails the two discriminating cases and leaves the two read_only guards passing, as intended. Its OpenAPI fixture is generated FROM the allow-list so it cannot drift. Plus an unpinned-request case in gate-guard.test.ts. 4928 green.
|
All five addressed in 1. Gate read-back was never enforced (
|
One textual conflict: src/pages/group-wizard.tsx, adjacent import lines — main added useAvailableStyles/isStyleSupported, this branch added RefetchErrorNotice. Kept both. Checked what the merge changed underneath, since none of it conflicts textually: - AppLayout still applies p-6 inside @container/main mx-auto max-w-screen-2xl, so the double-padding fix still holds, and no page has reacquired its own p-6. - Sidebar and TopBar props are unchanged, so the design-sync previews still typecheck against them. - Every count the docs assert is unchanged: 11 ui / 13 shared / 7 layout, 40 pages, 29 space-y-6, 19 ErrorState, 9 EmptyState, 6/6 ViewToggle persisted, h1 sizes 22 text-3xl to 18 text-2xl. - All five --color-sidebar* tokens still reach :root. - All 11 locales are valid JSON and still carry gdpr.statusUnknown. - HANDOFF.md auto-merged without losing any of main's operator edits (WRITE_ENDPOINTS 22 -> 24, gate-guard, the EDDI 6.2.0+ floor). One regression the merge introduces, recorded in .design-sync/NOTES.md rather than silently absorbed: PR #137 moved OperatorDrawer out of AppLayout and into top-bar.tsx as a static import. TopBar is a synced component, so the design-system bundle grew 4.00 -> 4.17 MB (+179 KB) and now carries the operator tool-scope allow-list. Monaco is still excluded. NOTES.md documents the two ways out (lazy-load the drawer, or drop TopBar from the surface) and the esbuild command to measure either. Gates on the merged tree: 4977 tests passing (331 files), build green, tsc -b green.
TopBar is a synced component and, since PR #137 moved the operator launcher into both shells' headers, it statically imports OperatorDrawer — dragging the tool-scope allow-list (WRITE_ENDPOINTS), the activation flow and the operator chat into _ds_bundle.js. That is the exact creep the scoped entry exists to prevent. cfg.tsconfig now points at a new tsconfig.ds-bundle.json whose paths map @/components/operator/operator-drawer to .design-sync/stubs/operator-drawer.tsx. The stub renders the launcher button in its resting state and nothing else — the panel is behaviour, not design surface, needing an operator config, a chat transcript and an approval stream that no preview has. Measured through the converter's own resolver, not a stand-in: tool-scopes, WRITE_ENDPOINTS, operator-activation and useOperatorChat all go to 0, and the bundle drops 4.36 -> 4.26 MB (-99 KB). monaco/vscode stay 0. tsconfig.design-sync.json now EXTENDS the bundle tsconfig instead of repeating its paths, so the type-check and the bundle can never disagree about where an import resolves — which means tsc -b validates the stub against TopBar's real usage. Verified by giving the stub a required prop: top-bar.tsx:208 fails with TS2741. Lazy-loading was tested first and does not work. The converter emits format: 'iife', which esbuild cannot code-split, so React.lazy(() => import(...)) is inlined into the same file — a probe entry doing nothing but that still carried tool-scopes and WRITE_ENDPOINTS. OperatorDrawer also renders the launcher itself, so it is mounted on every page and would load immediately anyway. Landmine found and pinned along the way: tsconfigPathsPlugin strips comments with a regex and JSON.parses the result, returning null on any throw — which silently drops alias resolution for the WHOLE bundle rather than failing. The "@/*" key contains a block-comment opener, so one stray closer anywhere after it deletes the entire paths object. tsconfig.design-sync.json already tripped this via its include globs, which is why the converter reads a dedicated glob-free file. design-sync-tsconfig.test.ts pins the parse, the wildcard, the stub mapping, its ordering ahead of the wildcard, and the absence of a closing delimiter. Tests 5043 -> 5047.
Makes the Platform Operator actually able to operate the platform, moves its launcher somewhere sensible, and standardises the default model. Pairs with labsai/EDDI#668 (docs-for-agents, MCP resource bridge, strict task-level
toolApprovals) — this PR works against today's backend and does not depend on that one merging.1. Write by default
read_writeis now the default scope, freely selectable on first activation; read-only is the explicit opt-down.The old flow demanded a two-step bootstrap — activate read-only, come back, reconfigure once a gate had been verified — because
isWriteScopeAvailablegated the offer ongate.verified, which cannot exist before an operator does. That precondition was weaker than what activation already proves about the agent it is actually creating:verifyGateInstalledreads the gate back from the just-provisioned document, andSo a first-activation write grant cannot survive a broken gate — it is refused, not risked.
isWriteScopeAvailablenow only checks thatWRITE_ENDPOINTSis non-empty.Caller-identity is no longer demanded either: on an OIDC deployment
authMode: nonecannot activate at all (the form blocks it — tool calls would 401), and on a no-auth deployment there are no identities to attribute approvals to. Requiring it made write scope permanently unreachable exactly where EDDI gets evaluated.2. LLM configs are editable — behind a hard gate guard
llmstorewrites are granted, so the operator can finally edit an agent's system prompt and model. That was the single largest capability gap: previously it could change rules, outputs, slot-filling and tool wiring, but not what the agent actually says.That document is also the one writable store that can carry a gate —
LlmConfiguration.Task.toolApprovalsfully replaces the agent-levelhitlConfig.toolApprovals(LlmTask.java). So the grant is valid only alongside the newgate-guard.ts, which is a control rather than a warning (it disables Approve outright, same class asself-guard.ts):toolApprovalsat any depth is refused;Absence is safe, not merely unchecked. A body without
toolApprovalsleaves the fieldnull, so the agent-level gate applies. Falling back can only leave a task as protected as its own agent already was; an agent with no gate was already ungated. Combined withself-guard.ts(which blocks the self-targetedupdateResourceUrithat is the only way to make an llm edit take effect on itself), the self-ungating loop stays closed.blocked-calls.tsnow resolves both guards in one place — the three approval surfaces were each inliningfindSelfTargetedCallsplus a copy of its i18n string, and a second guard would have triplicated that. A control that fires on one surface and not the others is a control with a documented bypass.3. Docs access, and the architecture in the prompt
GET /administration/docs{,/{name}}are added toREAD_ENDPOINTS. The backend built these in EDDI 6.2.0 specifically so an OpenAPI-generated agent picks them up as ordinary tools — theeddi://docs/*MCP resources covering the same files do not reach an agent, because EDDI's MCP client consumes tools and never callsresources/read. The allow-list simply never included them.The system prompt also gains the platform's versioning model — agent → workflow → config by id and version, nothing edits in place — in both scopes, since "my change did nothing" is a read-only diagnosis question whose answer is the version chain.
4. Launcher moved from a floating FAB into both headers
The bottom-right position was collision avoidance, not design: that corner already holds sonner's toast viewport (
z-999999999, which covered the launcher whenever a toast was up),ChatDrawer's composer,WorkforceBottomTabs, andworkforce-dashboard's ownMobileFab— the last of which won the hit test, so tapping the operator navigated to/workforce/new. Each collision bought a hardcoded offset (bottom-24,bottom-40) that held until the next thing landed there. It also had to be mounted four times, becauseAppLayoutandWorkforceLayout's three viewport branches share no chrome slot.The header is that slot. The drawer is now an inline icon button plus a panel anchored to it, so no offset is hardcoded at all; tab order improves as a side effect. Verified live at desktop and mobile widths.
5.
claude-sonnet-5everywhereThe
anthropicprovider default fed the agent wizard, group wizard, operator activation and Workforce team builder. Updated there plus every hardcoded copy: fourgroup-wizardfallbacks, the team-builder placeholder, task memory, the dream loop, the cascade step card, andcascadeModelNameacross all 11 locales. Bedrock's staleanthropic.claude-sonnet-4-6-v1:0becomesanthropic.claude-sonnet-5.Stale copy fixed alongside: the dashboard discovery card and the operator empty state both still described a read-only operator.
Tests
4923 passing (328 files), type check and lint clean. New:
gate-guard.test.ts(16). Rewritten to pin the new contracts rather than the old ones: the write-scope selection suite, theprovisionOperatorscope tests, and the operator-page scope chip.