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 all commits
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
13 changes: 11 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,17 @@ 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. Writes are unreachable until an approval
handler exists (`isWriteScopeAvailable`).
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.
- **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
18 changes: 17 additions & 1 deletion HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,21 @@
- **Tests**: 119 unit tests across debug panel + workforce components, audit test fix (camelCase assertion).
- **Commits**: 6 commits on `test/debug-workforce-coverage` from `e05391bf` to `feeb5aef`.

- **Platform Operator — Approval Gate + Write Capability** (branch: `feat/operator-write-scope`, stacked on the P1 read-only operator): Takes the operator from read-only to an admin-grantable `read_write` scope, gated end-to-end by a real human-approval workflow rather than a config toggle. Includes:
- **HITL approval gate (read-only)**: `useVerifyOperatorGate` builds and deploys a throwaway agent config against the backend to confirm the HITL tool-approval gate is actually enforced on this deployment before anything write-capable is ever offered — a version bump alone proves nothing. `gate.verified` is the seam every later write-scope check reads.
- **Approval surface**: `ApprovalBanner`'s `renderCallExtra` render-prop lets the operator screen show gated-call context inline in the operator chat itself (`operator-chat.tsx`) rather than sending the admin to a separate approvals page — resolving a pause is `requireExplicitPerCall` so a swept-in call can't inherit a top-level Approve unreviewed.
- **System prompt from the granted endpoint set**: the operator's system prompt is derived from `tool-scopes.ts`'s allow-list rather than hand-maintained prose, so the model is never told about more than it can actually call.
- **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.
- **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.
- **Commits**: 11 commits on `feat/operator-write-scope` from `fa793a04` to `6f47e835`; substantially more has landed on this branch since (request pinning/redaction hardening, self-guard, escalation flags, the approvals inbox, full agent/group create+modify, and the docked drawer below) — see `git log feat/operator-write-scope` rather than trusting this range.
- **Context-aware side-chat drawer**: the operator was Manager-only and full-page-only — unreachable from Workforce, and always a full navigation away. `operator-drawer.tsx` is a floating launcher mounted once in `AppLayout` and once in each of `WorkforceLayout`'s three viewport branches (self-positioned `fixed`, since those four layouts share no common chrome slot), reusing `useOperatorChat`/`useOperatorConfig` directly — same react-query cache, same conversation, not a second copy. Getting there required promoting `use-operator-chat.ts` off local `useState` onto a Zustand store (`useOperatorChatStore`) so the drawer and the full page render the identical live conversation; the full page's own call sites are unchanged. A pause opens to a compact notice + link to `/manage/operator` rather than a shrunk `ApprovalBanner` — that component is security-reviewed for one full-width surface, not a duplicate. `useCurrentScreenContext()` (route → `{screen, agentId, workflowId, groupId, boardId}` via `matchPath`, since the drawer sits above the routed `<Outlet/>`) threads into `send()` as a `context` argument, which a new unconditional, Qute-conditional section of the system prompt (`BODY_APP_CONTEXT`) reads as `{context.screen}` etc. — existing, previously-unused transport (`InputData.context`), zero backend changes. i18n: `operator.chat.pauseCompact{Fallback,Review}`, `operator.drawer.{title,notActivated,activate}` — all 11 locales.
- **Still open**: iteration 7 (agent/group authoring UI) and a final whole-branch review have both since landed — nothing tracked as open from this line as of the drawer above; check `git log` for what came after.

### Boardroom v2 Design Decisions

**Decisions confirmed by user:**
Expand Down Expand Up @@ -136,7 +151,8 @@
- 112 Backend tenancy tests passing (`mvn test`)

### Last Commit Focus
- Frontend: `feat(operator): add the Platform Operator agent (P1, read-only)` on `feat/platform-operator-agent`
- Frontend: `feat(operator): render the backend's resolved-request preview in the approval banner` (`6f47e835`) on `feat/operator-write-scope` — see the "Platform Operator — Approval Gate + Write Capability" phase above for the full arc. Not yet pushed; ask before pushing.
- Earlier frontend: `feat(operator): add the Platform Operator agent (P1, read-only)` on `feat/platform-operator-agent`
- Opt-in, admin-activated agent that inspects this EDDI deployment through its own REST API, exposed as tools via `setup-api`. Read-only allow-list (`src/lib/operator/tool-scopes.ts`), non-editable safety preamble, single-blob `platform.operator` config, activation flow with a post-provision spec check, operator screen with a live tool-activity trace, dashboard discovery card, kill switch, `operator.*` i18n across 11 locales.
- **Design correction:** the design assumed EDDI forwards the caller's token to an API agent's tool calls. It did not. That gap is now closed in the backend (labsai/EDDI#613) by a `${caller:token}` resolver, and the operator's `authMode: "caller-identity"` uses it: EDDI substitutes the token while building the request, releasing it only for a same-origin call, only into a header, and never persisting it. `"none"` remains the default and is blocked at activation when OIDC is on, since every tool call would 401.
- **Review pass:** added the post-deploy canary the design asked for (one probe read; it counts tool calls and detects 401s, since a READY badge proves nothing about whether the generated tools can authenticate), made activation honour setup-api's `deployed`/`deploymentStatus` and reject the `"unknown"` agent-id fallback, added a redeploy-in-place path so re-enabling a paused operator no longer rebuilds it, fixed the activation form's accessibility (not one control had an accessible name), and stopped `{{count}}` triggering i18next pluralization.
Expand Down
Loading