Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,8 @@ function buildApprovalSubject({
};
}
case "tool_use": {
// Declarative base only: no producer emits this subject until WS5
// designs the tool-use approval surface.
// The ACP bridge raises this for generic tool permissions; the subject's
// presentation is the banner's base until WS5 designs the tool-use surface.
const detailLines =
formatPendingInteractionSubjectDetailLines(interaction);
return {
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/data/interactions/approval-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ export function describeApprovalSubject(
plan: subject.plan,
detailLines: subject.planFilePath ? [subject.planFilePath] : [],
};
// Declarative base only: no producer emits this subject until WS5
// designs the tool-use approval surface.
// The ACP bridge raises this for generic tool permissions; the subject's
// presentation is the banner's base until WS5 designs the tool-use surface.
case "tool_use":
return {
title: payload.reason ?? subject.presentation.label.pending,
Expand Down
1 change: 0 additions & 1 deletion apps/server/src/internal/interactive-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ function pendingInteractionBlockerLabel(
return "permission grant";
case "plan":
return "plan review";
// Declarative base only: no producer emits this subject until WS5.
case "tool_use":
return "tool-use approval";
default: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,12 +444,12 @@ export function appendPendingInteractionTimelineEvent(
// duplicate it.
case "plan":
return;
// Unsupported until WS5 (interactions): no producer raises tool_use yet,
// and the single interaction-lifecycle event it will ride does not exist.
// A tool-use approval has no timeline item of its own: the provider's own
// tool call (the ACP agent's tool_call with the same id) is the timeline
// record, and the banner renders the subject's presentation. The single
// interaction-lifecycle event it will ride is WS5's (interactions).
case "tool_use":
throw new Error(
"tool_use approval subjects are not produced until WS5 (interactions)",
);
return;
default:
return assertNever(subject, "Unsupported approval subject for timeline");
}
Expand Down Expand Up @@ -504,11 +504,10 @@ export function appendPendingInteractionTimelineEventInTransaction(
// already the timeline record.
case "plan":
return;
// Unsupported until WS5: see appendPendingInteractionTimelineEvent.
// See appendPendingInteractionTimelineEvent: the provider's own tool call
// is the timeline record.
case "tool_use":
throw new Error(
"tool_use approval subjects are not produced until WS5 (interactions)",
);
return;
default:
return assertNever(subject, "Unsupported approval subject for timeline");
}
Expand Down
28 changes: 28 additions & 0 deletions apps/server/test/helpers/pending-interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ type PermissionGrantApprovalPayloadOptions = {
availableDecisions?: PendingInteractionApprovalDecision[];
};

type ToolUseApprovalPayloadOptions = {
itemId?: string;
reason?: string | null;
tool?: string;
availableDecisions?: PendingInteractionApprovalDecision[];
};

type UserQuestionPayloadOptions = {
allowFreeText?: boolean;
multiSelect?: boolean;
Expand Down Expand Up @@ -122,6 +129,27 @@ export function createPermissionGrantApprovalPayload(
};
}

export function createToolUseApprovalPayload(
options: ToolUseApprovalPayloadOptions = {},
): ApprovalPendingInteractionPayload {
return {
kind: "approval",
subject: {
kind: "tool_use",
itemId: options.itemId ?? "item-tool-use-approval",
tool: options.tool ?? "fetch",
presentation: {
label: { pending: "Fetching", completed: "Fetched" },
icon: { glyph: "Globe" },
title: "Fetch docs",
},
},
reason: options.reason ?? null,
availableDecisions:
options.availableDecisions ?? defaultBinaryAvailableDecisions,
};
}

export function createUserQuestionPayload(
options: UserQuestionPayloadOptions = {},
): UserQuestionPendingInteractionPayload {
Expand Down
65 changes: 65 additions & 0 deletions apps/server/test/services/pending-interactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import {
createAllowForSessionResolution,
createAllowOnceResolution,
createCommandApprovalPayload,
createDenyResolution,
createFileChangeApprovalPayload,
createPermissionGrantApprovalPayload,
createToolUseApprovalPayload,
createUserAnswerResolution,
createUserQuestionPayload,
} from "../helpers/pending-interactions.js";
Expand Down Expand Up @@ -1044,6 +1046,69 @@ describe("pending interaction lifecycle", () => {
});
});

it("accepts tool-use approvals without a timeline item of their own", async () => {
// The ACP bridge raises tool_use for every permission that is neither a
// command nor a file change. The provider's own tool call is the timeline
// record, so the lifecycle appends no item event for the subject, and a
// denial settles it like any other approval.
await withTestHarness(async (harness) => {
const { host } = seedHostSession(harness.deps, {
id: "host-pending-interaction-tool-use",
});
const { project } = seedProjectWithSource(harness.deps, {
hostId: host.id,
});
const environment = seedEnvironment(harness.deps, {
hostId: host.id,
projectId: project.id,
});
const thread = seedThread(harness.deps, {
projectId: project.id,
environmentId: environment.id,
});

const created = registerPendingInteraction(
harness.deps,
harness.deps.pendingInteractions,
{
threadId: thread.id,
turnId: "turn-tool-use",
providerId: "codex",
providerThreadId: "provider-thread-tool-use",
providerRequestId: "request-tool-use",
payload: createToolUseApprovalPayload({ itemId: "mcp-call-1" }),
},
);
if (created.outcome === "rejected") {
throw new Error(
`Expected interaction registration to succeed: ${created.reason}`,
);
}
const itemEventsFor = () =>
harness.db
.select()
.from(eventTable)
.where(eq(eventTable.threadId, thread.id))
.all()
.filter((row) => row.type.startsWith("item/"));
expect(itemEventsFor()).toEqual([]);

expect(
harness.deps.pendingInteractions.resolvePendingInteraction({
threadId: thread.id,
interactionId: created.interaction.id,
resolution: createDenyResolution(),
}),
).toEqual(
expect.objectContaining({
status: "resolving",
resolution: expect.objectContaining({ decision: "deny" }),
}),
);
expect(itemEventsFor()).toEqual([]);
});
});

it("allows command approvals to grant explicit session permissions for session decisions", async () => {
await withTestHarness(async (harness) => {
const { host } = seedHostSession(harness.deps, {
Expand Down
3 changes: 2 additions & 1 deletion docs/provider-bridge-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ range is what gates a bridge: every bridge in this repo reports
`delegation` (`childRef`, `label`, `background`, `summary?`; one shape for
codex `spawnAgent`/`wait`, the Claude `Agent` tool, and backgrounded
agents, which replaced `thread/openWork`), and `planSteps` (a structured plan
snapshot as an item, beside the turn-level `turn.plan`).
snapshot as an item, which replaced the turn-level `turn.plan` delta once
the ACP bridge — its last speaker — migrated).
- **`presentation`** on `item.open` and `item.close`, the one place it
travels: `label {pending, completed}`, `icon {glyph}` (host glyphs only —
a plugin-relative asset path cannot outlive the plugin, and a durable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -730,8 +730,11 @@ function expectSemanticApprovalRequest(
expect(request.payload.subject.plan.length).toBeGreaterThan(0);
break;
case "tool_use":
// Unsupported until WS5: no fake or real producer raises it yet.
throw new Error("tool_use approval subjects are not produced until WS5");
expect(request.payload.subject.tool.length).toBeGreaterThan(0);
expect(
request.payload.subject.presentation.label.pending.length,
).toBeGreaterThan(0);
break;
}
expect(request.payload.availableDecisions.length).toBeGreaterThan(0);
for (const decision of request.payload.availableDecisions) {
Expand Down
4 changes: 2 additions & 2 deletions packages/core-ui/src/pending-interaction-formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ export function formatPendingInteractionSubjectDetailLines(
: [];
}
case "tool_use": {
// Declarative base only: no producer emits this subject until WS5
// rewires the interaction producers and designs the tool-use surface.
// Raised by the ACP bridge for generic tool permissions; WS5 designs the
// tool-use surface.
const { tool, presentation } = interaction.payload.subject;
return [
`Tool: ${tool}`,
Expand Down
4 changes: 2 additions & 2 deletions packages/domain/src/pending-interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,8 @@ const pendingInteractionPlanApprovalSubjectSchema = z.object({
* kind). Policy-bearing like the other approval subjects: `auto` approves it,
* `accept-edits` asks. `presentation` is the bridge's declarative rendering
* of the call, so the approval banner reads the same on every client with no
* tool-name table. Not yet produced by any bridge: WS5 (interactions) rewires
* the producers.
* tool-name table. The ACP bridge raises it for every permission that is
* neither a command nor a file change; WS5 (interactions) rewires the rest.
*/
export const pendingInteractionToolUseApprovalSubjectSchema = z.object({
kind: z.literal("tool_use"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
{"ts":1787275210707,"run":1787275054108,"seq":420.48387096774195,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"deltas\":[{\"kind\":\"item.textClose\",\"key\":{\"channel\":\"thought\"},\"channel\":\"reasoningText\"},{\"kind\":\"item.textDelta\",\"key\":{\"channel\":\"assistant\"},\"channel\":\"agentMessage\",\"text\":\" with\",\"noTurnFallback\":{\"raw\":{\"jsonrpc\":\"2.0\",\"method\":\"acp/update\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"update\":{\"sessionUpdate\":\"agent_message_chunk\",\"content\":{\"type\":\"text\",\"text\":\" with\"}}}},\"rawType\":\"acp/update:agent_message_chunk\"}}]}}"}
{"ts":1787275210708,"run":1787275054108,"seq":420.51612903225805,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"deltas\":[{\"kind\":\"item.textClose\",\"key\":{\"channel\":\"thought\"},\"channel\":\"reasoningText\"},{\"kind\":\"item.textDelta\",\"key\":{\"channel\":\"assistant\"},\"channel\":\"agentMessage\",\"text\":\" done\",\"noTurnFallback\":{\"raw\":{\"jsonrpc\":\"2.0\",\"method\":\"acp/update\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"update\":{\"sessionUpdate\":\"agent_message_chunk\",\"content\":{\"type\":\"text\",\"text\":\" done\"}}}},\"rawType\":\"acp/update:agent_message_chunk\"}}]}}"}
{"ts":1787275210709,"run":1787275054108,"seq":420.5483870967742,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"deltas\":[{\"kind\":\"item.textClose\",\"key\":{\"channel\":\"thought\"},\"channel\":\"reasoningText\"},{\"kind\":\"item.textDelta\",\"key\":{\"channel\":\"assistant\"},\"channel\":\"agentMessage\",\"text\":\".\",\"noTurnFallback\":{\"raw\":{\"jsonrpc\":\"2.0\",\"method\":\"acp/update\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"update\":{\"sessionUpdate\":\"agent_message_chunk\",\"content\":{\"type\":\"text\",\"text\":\".\"}}}},\"rawType\":\"acp/update:agent_message_chunk\"}}]}}"}
{"ts":1787275210710,"run":1787275054108,"seq":420.5806451612903,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"deltas\":[{\"kind\":\"item.textClose\",\"key\":{\"channel\":\"thought\"},\"channel\":\"reasoningText\"},{\"kind\":\"item.textClose\",\"key\":{\"channel\":\"assistant\"},\"channel\":\"agentMessage\"},{\"kind\":\"item.open\",\"key\":{\"providerItemId\":\"call-a5291c91-4515-4485-a393-4d0985ceacd6-0\\nfc_1b010475-e3b1-9da1-a50b-fe717290b58d_0\"},\"item\":{\"type\":\"command\",\"command\":\"touch approved.txt\",\"cwd\":\"\"},\"noTurnFallback\":{\"raw\":{\"jsonrpc\":\"2.0\",\"method\":\"acp/update\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"update\":{\"toolCallId\":\"call-a5291c91-4515-4485-a393-4d0985ceacd6-0\\nfc_1b010475-e3b1-9da1-a50b-fe717290b58d_0\",\"title\":\"`touch approved.txt`\",\"kind\":\"execute\",\"status\":\"pending\",\"rawInput\":{\"command\":\"touch approved.txt\"},\"sessionUpdate\":\"tool_call\"}}},\"rawType\":\"acp/update:tool_call\"}}]}}"}
{"ts":1787275210710,"run":1787275054108,"seq":420.5806451612903,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"deltas\":[{\"kind\":\"item.textClose\",\"key\":{\"channel\":\"thought\"},\"channel\":\"reasoningText\"},{\"kind\":\"item.textClose\",\"key\":{\"channel\":\"assistant\"},\"channel\":\"agentMessage\"},{\"kind\":\"item.open\",\"key\":{\"providerItemId\":\"call-a5291c91-4515-4485-a393-4d0985ceacd6-0\\nfc_1b010475-e3b1-9da1-a50b-fe717290b58d_0\"},\"item\":{\"type\":\"command\",\"command\":\"touch approved.txt\",\"cwd\":\"\"},\"presentation\":{\"label\":{\"pending\":\"Running command\",\"completed\":\"Ran command\"},\"icon\":{\"glyph\":\"Terminal\"},\"title\":\"touch approved.txt\"},\"noTurnFallback\":{\"raw\":{\"jsonrpc\":\"2.0\",\"method\":\"acp/update\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"update\":{\"toolCallId\":\"call-a5291c91-4515-4485-a393-4d0985ceacd6-0\\nfc_1b010475-e3b1-9da1-a50b-fe717290b58d_0\",\"title\":\"`touch approved.txt`\",\"kind\":\"execute\",\"status\":\"pending\",\"rawInput\":{\"command\":\"touch approved.txt\"},\"sessionUpdate\":\"tool_call\"}}},\"rawType\":\"acp/update:tool_call\"}}]}}"}
{"ts":1787275210711,"run":1787275054108,"seq":420.61290322580646,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"deltas\":[{\"kind\":\"unhandled\",\"raw\":{\"jsonrpc\":\"2.0\",\"method\":\"acp/update\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"update\":{\"toolCallId\":\"call-a5291c91-4515-4485-a393-4d0985ceacd6-0\\nfc_1b010475-e3b1-9da1-a50b-fe717290b58d_0\",\"status\":\"in_progress\",\"sessionUpdate\":\"tool_call_update\"}}},\"rawType\":\"acp/update:tool_call_update\",\"vouchedTurn\":false,\"onlyIfNoTurn\":true}]}}"}
{"ts":1787275210712,"run":1787275054108,"seq":420.64516129032256,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"interaction/request\",\"params\":{\"providerThreadId\":\"5d5e98e5-e088-4805-af26-5243072c5360\",\"threadId\":\"thr_zbuujsu6ym\",\"turnId\":null,\"payload\":{\"kind\":\"approval\",\"subject\":{\"kind\":\"command\",\"itemId\":\"call-a5291c91-4515-4485-a393-4d0985ceacd6-0\\nfc_1b010475-e3b1-9da1-a50b-fe717290b58d_0\",\"command\":\"`touch approved.txt`\",\"cwd\":null,\"actions\":[{\"type\":\"unknown\",\"command\":\"`touch approved.txt`\"}],\"sessionGrant\":null},\"reason\":null,\"availableDecisions\":[\"allow_once\",\"allow_for_session\",\"deny\"]}}}"}
{"ts":1787275210713,"run":1787275054108,"seq":420.6774193548387,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"deltas\":[{\"kind\":\"item.close\",\"key\":{\"providerItemId\":\"call-a5291c91-4515-4485-a393-4d0985ceacd6-0\\nfc_1b010475-e3b1-9da1-a50b-fe717290b58d_0\"},\"status\":\"completed\",\"resultText\":\"{\\\"exitCode\\\":0,\\\"stdout\\\":\\\"\\\",\\\"stderr\\\":\\\"\\\"}\",\"aggregatedOutput\":\"{\\\"exitCode\\\":0,\\\"stdout\\\":\\\"\\\",\\\"stderr\\\":\\\"\\\"}\",\"exitCode\":0,\"item\":{\"type\":\"command\",\"command\":\"touch approved.txt\",\"cwd\":\"\"},\"noTurnFallback\":{\"raw\":{\"jsonrpc\":\"2.0\",\"method\":\"acp/update\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"update\":{\"toolCallId\":\"call-a5291c91-4515-4485-a393-4d0985ceacd6-0\\nfc_1b010475-e3b1-9da1-a50b-fe717290b58d_0\",\"status\":\"completed\",\"rawOutput\":{\"exitCode\":0,\"stdout\":\"\",\"stderr\":\"\"},\"sessionUpdate\":\"tool_call_update\"}}},\"rawType\":\"acp/update:tool_call_update\"}}]}}"}
{"ts":1787275210713,"run":1787275054108,"seq":420.6774193548387,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"deltas\":[{\"kind\":\"item.close\",\"key\":{\"providerItemId\":\"call-a5291c91-4515-4485-a393-4d0985ceacd6-0\\nfc_1b010475-e3b1-9da1-a50b-fe717290b58d_0\"},\"status\":\"completed\",\"resultText\":\"{\\\"exitCode\\\":0,\\\"stdout\\\":\\\"\\\",\\\"stderr\\\":\\\"\\\"}\",\"aggregatedOutput\":\"{\\\"exitCode\\\":0,\\\"stdout\\\":\\\"\\\",\\\"stderr\\\":\\\"\\\"}\",\"exitCode\":0,\"item\":{\"type\":\"command\",\"command\":\"touch approved.txt\",\"cwd\":\"\"},\"presentation\":{\"label\":{\"pending\":\"Running command\",\"completed\":\"Ran command\"},\"icon\":{\"glyph\":\"Terminal\"},\"title\":\"touch approved.txt\"},\"noTurnFallback\":{\"raw\":{\"jsonrpc\":\"2.0\",\"method\":\"acp/update\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"update\":{\"toolCallId\":\"call-a5291c91-4515-4485-a393-4d0985ceacd6-0\\nfc_1b010475-e3b1-9da1-a50b-fe717290b58d_0\",\"status\":\"completed\",\"rawOutput\":{\"exitCode\":0,\"stdout\":\"\",\"stderr\":\"\"},\"sessionUpdate\":\"tool_call_update\"}}},\"rawType\":\"acp/update:tool_call_update\"}}]}}"}
{"ts":1787275210714,"run":1787275054108,"seq":420.7096774193548,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"deltas\":[{\"kind\":\"item.textDelta\",\"key\":{\"channel\":\"thought\"},\"channel\":\"reasoningText\",\"text\":\"The command succeeded.\",\"noTurnFallback\":{\"raw\":{\"jsonrpc\":\"2.0\",\"method\":\"acp/update\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"update\":{\"sessionUpdate\":\"agent_thought_chunk\",\"content\":{\"type\":\"text\",\"text\":\"The command succeeded.\"}}}},\"rawType\":\"acp/update:agent_thought_chunk\"}}]}}"}
{"ts":1787275210715,"run":1787275054108,"seq":420.741935483871,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"deltas\":[{\"kind\":\"item.textDelta\",\"key\":{\"channel\":\"thought\"},\"channel\":\"reasoningText\",\"text\":\" I will reply with the\",\"noTurnFallback\":{\"raw\":{\"jsonrpc\":\"2.0\",\"method\":\"acp/update\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"update\":{\"sessionUpdate\":\"agent_thought_chunk\",\"content\":{\"type\":\"text\",\"text\":\" I will reply with the\"}}}},\"rawType\":\"acp/update:agent_thought_chunk\"}}]}}"}
{"ts":1787275210716,"run":1787275054108,"seq":420.7741935483871,"dir":"bridge→runtime","line":"{\"jsonrpc\":\"2.0\",\"method\":\"thread/delta\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"deltas\":[{\"kind\":\"item.textDelta\",\"key\":{\"channel\":\"thought\"},\"channel\":\"reasoningText\",\"text\":\" single word \\\"done\\\".\",\"noTurnFallback\":{\"raw\":{\"jsonrpc\":\"2.0\",\"method\":\"acp/update\",\"params\":{\"threadId\":\"thr_zbuujsu6ym\",\"update\":{\"sessionUpdate\":\"agent_thought_chunk\",\"content\":{\"type\":\"text\",\"text\":\" single word \\\"done\\\".\"}}}},\"rawType\":\"acp/update:agent_thought_chunk\"}}]}}"}
Expand Down
Loading
Loading