mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-03 04:49:26 +00:00
The mode from ocw-context/docs/reviewed-auto-mode.md (rev. 4), v1 scope.
coworker/reviewer.py (new)
- The 8.3 prompt verbatim, cache-shaped: instructions + known world (folders
and remotes only) + user-message history in the stable prefix; this turn's
request and ONE action in the suffix.
- parse_verdict: any defect (empty, non-JSON, unknown verdict) -> unsure.
There is no parse path that results in execution (8.5).
- Reviewer.review never raises: provider errors and timeouts -> unsure.
Metering counters (checks / verdicts / tokens) for 1.7.
- AGENT_DENY_MESSAGE: the terse, non-diagnostic refusal the agent gets on a
deny; the full reason goes to the user only (8.4 asymmetry).
coworker/engine.py
- Reviewer consulted ONLY when: attached, mode is AUTO_APPROVE, session
explicitly attended (unset is_attended counts as NOT attended, so
automations can never be reviewed), fewer than two denials this turn.
- Consulted ONLY on decisions the gate marked needs_user - hard denies
never reach it, so it can only turn "ask" into "allow" (1.2).
- One action per request, fired concurrently for all of a turn's escalating
calls before the sequential authorize loop (8.6): a verdict cannot land
on the wrong action, and approval cards still reach the human one at a
time in call order.
- allow -> runs, audited with the reason. deny -> blocked; user event
carries the full reviewer reason + allow_anyway; agent message carries
only AGENT_DENY_MESSAGE. unsure -> today's card.
- Reviewer sees the user's words only, extracted mechanically from
role=user messages - never agent output, never tool results (4.4).
coworker/permissions.py
- Mode.AUTO renamed Mode.BYPASS_APPROVALS ("bypass-approvals"); legacy
"auto" still parses via _missing_ so configs, saved sessions, and the
golden decision table are untouched.
- Mode.AUTO_APPROVE ("auto-approve"): gate-identical to INTERACTIVE except
session grants ("always allow this ...") no longer auto-allow - they
route to the reviewer instead (1.5: out-of-band standing policy may skip
the judge; an in-flow click may not). Config allowlists still skip.
- _domain_allowed(include_session=False) checks the user-settings list only.
coworker/config.py: auto_approve flag, off by default, _GLOBAL_ONLY (a
cloned repo cannot hand itself a looser reviewer). agent.py attaches the
Reviewer only when the flag is on; without it AUTO_APPROVE behaves exactly
like INTERACTIVE.
server/manager.py: autonomy audit ranks auto-approve above interactive
(turning the reviewer on IS raising autonomy) and below bypass.
GUI: mode picker label "Full access" -> "Bypass approvals" (wire value
"auto" kept). Verified live against the real sidecar; e2e spec updated;
tsc and all 111 GUI unit tests pass.
Tests: tests/test_auto_approve.py (33) - gate behaviour per mode, fail-
closed parsing, prompt shape, deny asymmetry, retry guard, attended
gating, hard-deny isolation, per-action verdict landing, and that the
reviewer never sees agent prose. Permission suites + golden table: 146
passing unchanged.
99 lines
4.7 KiB
TypeScript
99 lines
4.7 KiB
TypeScript
import { test, expect } from "./fixtures";
|
|
|
|
// Guards the three-control composer row (§22): send-gating (accent only with content), the "+"
|
|
// attach menu, and the Mode menu (permission options + the folded-in Send-to-Inbox toggle).
|
|
test("composer: send-gating, + attach menu, Mode menu", async ({ page }) => {
|
|
await page.goto("/");
|
|
await page.getByText("Draft the launch note").first().click();
|
|
|
|
const box = page.getByPlaceholder(/Ask the coworker/);
|
|
const send = page.getByRole("button", { name: "Send" });
|
|
|
|
// Send is subtle grey when empty, accent once there's content, grey again when cleared.
|
|
await expect(send).not.toHaveClass(/bg-accent/);
|
|
await box.fill("hello there");
|
|
await expect(send).toHaveClass(/bg-accent/);
|
|
await box.fill("");
|
|
await expect(send).not.toHaveClass(/bg-accent/);
|
|
|
|
// "+" attach menu offers the three typed shortcuts.
|
|
await page.getByRole("button", { name: "Attach" }).click();
|
|
await expect(page.getByRole("button", { name: "Photo or image" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "PDF", exact: true })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Other files" })).toBeVisible();
|
|
// Clicking the backdrop closes it.
|
|
await page.locator(".fixed.inset-0.z-30").click();
|
|
await expect(page.getByRole("button", { name: "Photo or image" })).toHaveCount(0);
|
|
|
|
// Mode menu: the three shipped permission options with the current one marked, plus the
|
|
// Unattended/send-to-Inbox toggle (§22). Plan + Custom hidden for this release (2026-07-22).
|
|
await page.getByRole("button", { name: "Mode", exact: true }).click();
|
|
const menu = page.getByTestId("mode-menu");
|
|
await expect(menu.getByText("Discuss")).toBeVisible();
|
|
await expect(menu.getByText("Plan", { exact: true })).toHaveCount(0);
|
|
await expect(menu.getByText("Custom", { exact: true })).toHaveCount(0);
|
|
// The current mode is marked with a ✓.
|
|
await expect(menu.locator("button").filter({ hasText: "Ask for approval" })).toContainText("✓");
|
|
await expect(menu.getByRole("switch", { name: "Send approvals to the Inbox" })).toBeVisible();
|
|
// Picking an option closes the menu (and would flip the live engine's mode).
|
|
await menu.getByText("Bypass approvals").click();
|
|
await expect(page.getByTestId("mode-menu")).toHaveCount(0);
|
|
});
|
|
|
|
// PDFs read as data URLs and show a named chip (DMG #29 walkthrough catch: PDFs silently
|
|
// no-op'd because readFile only handled images and text).
|
|
test("composer: picking a PDF shows an attachment chip and arms send", async ({ page }) => {
|
|
await page.goto("/");
|
|
await page.getByText("Draft the launch note").first().click();
|
|
|
|
const send = page.getByRole("button", { name: "Send" });
|
|
await expect(send).not.toHaveClass(/bg-accent/);
|
|
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: "report.pdf",
|
|
mimeType: "application/pdf",
|
|
buffer: Buffer.from("%PDF-1.4\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF"),
|
|
});
|
|
|
|
const chip = page.locator(".attach-chip");
|
|
await expect(chip).toContainText("report.pdf");
|
|
await expect(send).toHaveClass(/bg-accent/); // attachment alone arms send
|
|
|
|
// Removing the chip disarms send again.
|
|
await chip.locator(".attach-x").click();
|
|
await expect(page.locator(".attach-chip")).toHaveCount(0);
|
|
await expect(send).not.toHaveClass(/bg-accent/);
|
|
});
|
|
|
|
// Token-savings threshold (owner ask, 2026-07-17): a PDF over the user's page limit is
|
|
// REJECTED with a visible notice — no chip, send stays disarmed. Fixture limit: 2 pages;
|
|
// the mock inspect endpoint reads the page count from a "%%pages=N" marker in the body.
|
|
test("composer: PDF over the page threshold is rejected with a notice", async ({ page }) => {
|
|
await page.goto("/");
|
|
await page.getByText("Draft the launch note").first().click();
|
|
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: "big-report.pdf",
|
|
mimeType: "application/pdf",
|
|
buffer: Buffer.from("%PDF-1.4\n%%pages=34\ntrailer\n<<>>\n%%EOF"),
|
|
});
|
|
|
|
const notice = page.getByTestId("attach-notice");
|
|
await expect(notice).toContainText("big-report.pdf skipped");
|
|
await expect(notice).toContainText("34 pages is over your 2-page limit");
|
|
await expect(page.locator(".attach-chip")).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: "Send" })).not.toHaveClass(/bg-accent/);
|
|
|
|
// The ✕ dismisses the notice.
|
|
await notice.getByRole("button").click();
|
|
await expect(page.getByTestId("attach-notice")).toHaveCount(0);
|
|
|
|
// A small PDF (1 page per the mock) still attaches fine after a rejection.
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: "small.pdf",
|
|
mimeType: "application/pdf",
|
|
buffer: Buffer.from("%PDF-1.4\n%%pages=1\ntrailer\n<<>>\n%%EOF"),
|
|
});
|
|
await expect(page.locator(".attach-chip")).toContainText("small.pdf");
|
|
});
|