fix(runtime): stop gating pi tool calls; Co-Founder approvals live at API layer

The Co-Founder beforeToolCall hook (introduced in 77f81a7) gated every
non-readonly pi tool call as a `dangerous_command` approval. Because
agents call `POST /api/companies/:id/messages`, `/agents` (hire), and
every other company mutation via bash→node/curl, the first bash call in
every Co-Founder run got parked on a 30-minute approval timeout. Users
observed it as "agents never talk to each other" and "every bash command
waits for approval"; both symptoms are the same bug.

The business-level approval flow (`hire_agent`, `document_review`,
`plan_review`, etc. in execution-policy.ts) already gates the events
users actually care about, at the HTTP route layer where the event
originates (routes-agents.ts:368 for hiring, document routes for
deliverables). Stacking a pi-level hook on top was duplicate gating with
the wrong taxonomy.

Removed:
- src/main/agent-runtime/approval-hook.ts (entire file, 137 LOC)
- `beforeToolCall` conditional in worker-runner.ts and the
  corresponding RPC method in worker-types.ts and runtime-bridge.ts
- `approvalHook` field + `resolveApproval` callback + shutdown
  `cancelAll` in runtime-bridge.ts / service-graph-pi-runtime.ts /
  wire-api-server.ts

Unchanged:
- `dangerous_command` approval type itself (still used by automation
  workflows and routes-misc.ts)
- CEO mode (already never attached the hook)
- All business-level approval types and their wiring
This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 19:11:04 -04:00
1 parent 4cfb926971
commit 02ba887610
7 files changed
+8 -200

No files matched your search

+6 -23
View File
@@ -4,7 +4,6 @@ import type { DesktopEvent } from "@shared/types";
import { ensureTaskThread } from "@main/thread-integration/thread-helpers";
import { SqliteSessionStore } from "./sqlite-session-store";
import { CostWriter } from "./cost-writer";
import { HumanApprovalHook } from "./approval-hook";
import type { CredentialResolver } from "@main/credentials";
import { getConnectorDefinition } from "@main/connectors";
import { projectInboxMessageToPi } from "./inbox-projection";
@@ -133,23 +132,19 @@ interface ActiveRunContext {
* - Forwards `agent-event` from the worker to the renderer (push channel).
* - Persists each `message_end` message via `SqliteSessionStore`.
* - Calls `CostWriter` for assistant messages with usage.
* - Hosts `HumanApprovalHook` for Co-Founder mode tool gating; the worker
* only invokes the hook RPC when running in `ai_cofounder` mode, so
* CEO-mode runs pay zero IPC overhead.
*
* Co-Founder mode's human-review gates live at the API endpoint layer
* (hire_agent / document_review approvals created by `routes-*.ts`), not
* at the pi tool-call layer; the worker constructs `Agent` without a
* `beforeToolCall` hook in either mode.
*/
export class RuntimeBridge {
readonly approvalHook: HumanApprovalHook;
private readonly costWriter: CostWriter;
private readonly activeRuns = new Map<string, ActiveRunContext>();
private readonly sessionStores = new Map<string, SqliteSessionStore>();
constructor(private readonly deps: RuntimeBridgeDeps) {
this.costWriter = new CostWriter({ db: deps.db });
this.approvalHook = new HumanApprovalHook({
db: deps.db,
emitEvent: deps.emitEvent,
logger: deps.logger,
});
}
private registerActiveRun(context: ActiveRunContext): void {
@@ -299,7 +294,7 @@ export class RuntimeBridge {
/** Handles RPC requests issued by the worker (`rpc-request` envelope). */
async handleRpc(request: {
rpcId: string;
method: "persistMessage" | "prepareRun" | "beforeToolCall";
method: "persistMessage" | "prepareRun";
runId: string;
payload: unknown;
}): Promise<{ ok: true; result: unknown } | { ok: false; error: string }> {
@@ -309,18 +304,6 @@ export class RuntimeBridge {
this.getOrCreateStore(payload.taskSessionId).appendMessage(payload.message);
return { ok: true, result: undefined };
}
if (request.method === "beforeToolCall") {
const payload = request.payload as {
agentId: string;
companyId: string;
taskId: string | null;
toolCallId: string;
toolName: string;
args: unknown;
};
const verdict = await this.approvalHook.before(request.runId, payload);
return { ok: true, result: verdict };
}
if (request.method === "prepareRun") {
const payload = request.payload as {
connectorId: string;