diff --git a/coworker/engine.py b/coworker/engine.py
index 9b36e43d..874efe04 100644
--- a/coworker/engine.py
+++ b/coworker/engine.py
@@ -984,6 +984,20 @@ class TurnEngine:
self.tool_requester(dict(args), tool_call.id),
interrupted={"installed": False, "error": "interrupted by user"},
) or {"installed": False, "error": "no response"}
+ if not result.get("installed"):
+ # The card says "or install it yourself and continue" — honor it. A user
+ # who brewed the tool mid-prompt and clicked Continue has PROVIDED it,
+ # not declined it; find their copy before treating this as a refusal.
+ found = _toolchain.resolve(name)
+ if found:
+ result = {
+ "installed": True,
+ "path": found,
+ "note": (
+ "the user provided their own copy instead of the managed "
+ "install — use it from this path"
+ ),
+ }
if not result.get("installed"):
result.setdefault(
"guidance",
diff --git a/surfaces/gui/e2e/toolreq.spec.ts b/surfaces/gui/e2e/toolreq.spec.ts
index fb812a0e..7efbf64f 100644
--- a/surfaces/gui/e2e/toolreq.spec.ts
+++ b/surfaces/gui/e2e/toolreq.spec.ts
@@ -22,8 +22,11 @@ test("request_tool surfaces a card naming the tool, the reason and the pinned ve
// the coworker's quoted reason (mixing them is what made the card confusing, 2026-08-14).
const facts = card.locator(".toolreq-facts");
await expect(facts).toContainText("8.30.1");
- await expect(facts).toContainText(/checksum-verified/i);
- await expect(facts).toContainText("from github.com/gitleaks");
+ // Plain-language consent: who installs (OpenWorker), from where, and the self-install
+ // alternative — no supply-chain jargon on the card (owner feedback 2026-08-15).
+ await expect(facts).toContainText(
+ "OpenWorker installs its own verified copy from github.com/gitleaks — or install it yourself and continue.",
+ );
// Declining must read as a normal choice that continues the run, not a failure.
await expect(card.getByTestId("toolreq-skip")).toHaveText("Continue without it");
});
diff --git a/surfaces/gui/src/components/ToolRequestCard.tsx b/surfaces/gui/src/components/ToolRequestCard.tsx
index 25963154..b22d1685 100644
--- a/surfaces/gui/src/components/ToolRequestCard.tsx
+++ b/surfaces/gui/src/components/ToolRequestCard.tsx
@@ -30,20 +30,25 @@ export function ToolRequestCard({
coworker's quoted ask above — mixing the two voices is what made the card confusing. */}
{item.installable ? (
-
- {item.tool}
- {item.version ? ` ${item.version}` : ""}
-
- {item.summary &&
{item.summary}}
-
pinned & checksum-verified
- {item.source &&
from {item.source}}
+
+
+ {item.tool}
+ {item.version ? ` ${item.version}` : ""}
+
+ {item.summary && {item.summary}}
+
+
+ OpenWorker installs its own verified copy
+ {item.source ? ` from ${item.source}` : ""} — or install it yourself and
+ continue.
+
) : (
-
- No verified build is available for this machine — install it yourself if you want
- this check, or continue and the coworker will note the gap.
-
+
+ OpenWorker has no verified build for this machine — install it yourself if you
+ want this check, or continue and the coworker will note the gap.
+
)}
diff --git a/surfaces/gui/src/styles.css b/surfaces/gui/src/styles.css
index 64effa43..6a02fffb 100644
--- a/surfaces/gui/src/styles.css
+++ b/surfaces/gui/src/styles.css
@@ -800,14 +800,15 @@ button.btn.danger { color: var(--accent); }
/* request_tool fact strip — the product's own metadata (version, publisher, checksum),
deliberately NOT italic so it can't be misread as the coworker still talking */
.toolreq-facts {
- display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px;
font-size: 12.5px; color: var(--muted); font-style: normal;
background: var(--paper); border: 1px solid var(--line); border-radius: 8px;
padding: 7px 10px; margin: 2px 0 10px;
}
.toolreq-facts code { color: var(--ink); font-size: 12.5px; }
-.toolreq-fact + .toolreq-fact::before,
-.toolreq-facts code + .toolreq-fact::before { content: "· "; color: var(--muted); }
+.toolreq-factrow { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
+.toolreq-factrow code + .toolreq-fact::before { content: "· "; color: var(--muted); }
+.toolreq-explain { margin-top: 4px; }
+.toolreq-factrow + .toolreq-explain { margin-top: 5px; }
/* a disabled Install must LOOK disabled — the whole card exists to not oversell */
.dirreq-actions .btn:disabled { opacity: 0.45; cursor: not-allowed; }
.dirreq-pathrow { display: flex; gap: 8px; align-items: center; }
diff --git a/tests/test_tool_request.py b/tests/test_tool_request.py
index de5624b1..6ca8e59b 100644
--- a/tests/test_tool_request.py
+++ b/tests/test_tool_request.py
@@ -68,9 +68,14 @@ async def test_emits_tool_requested_and_reports_install(tmp_path):
@pytest.mark.asyncio
-async def test_declining_tells_the_agent_to_fall_back_openly(tmp_path):
+async def test_declining_tells_the_agent_to_fall_back_openly(tmp_path, monkeypatch):
"""A refusal must not read as 'check done'. The tool result has to push the agent
toward a disclosed fallback, which is the whole point of the contract."""
+ from coworker import toolchain
+
+ # Truly absent — otherwise the decline-time re-check (below) would find the dev
+ # machine's real gitleaks and turn this into the user-provided-copy path.
+ monkeypatch.setattr(toolchain, "resolve", lambda name: None)
async def requester(args, tool_call_id=None):
return {"installed": False, "reason": "the user declined to install it"}
@@ -86,6 +91,29 @@ async def test_declining_tells_the_agent_to_fall_back_openly(tmp_path):
assert "degraded" in body or "fallback" in body
+@pytest.mark.asyncio
+async def test_decline_recheck_finds_a_copy_the_user_installed_themselves(tmp_path, monkeypatch):
+ """The card says "or install it yourself and continue" — that has to be real. A user
+ who brews the tool while the prompt is up and clicks Continue has PROVIDED the tool;
+ the agent must be handed their copy's path, not a refusal."""
+ from coworker import toolchain
+
+ monkeypatch.setattr(toolchain, "resolve", lambda name: "/opt/homebrew/bin/gitleaks")
+
+ async def requester(args, tool_call_id=None):
+ return {"installed": False, "reason": "the user declined to install it"}
+
+ engine = _engine(tmp_path, requester)
+ events = await _run(engine)
+ finished = [e for e in events if e.type is EventType.TOOL_FINISHED]
+ assert finished[0].data["status"] == "ok"
+
+ tool_msg = [m for m in engine.messages if m.get("role") == "tool"][-1]
+ body = str(tool_msg["content"])
+ assert "/opt/homebrew/bin/gitleaks" in body
+ assert "own copy" in body # attributed to the user, not to a managed install
+
+
@pytest.mark.asyncio
async def test_event_tells_the_truth_about_installability(tmp_path, monkeypatch):
"""Owner-hit 2026-08-14: the card offered Install for a tool with no pinned build —