diff --git a/maps/feature-map/questions-and-approvals.md b/maps/feature-map/questions-and-approvals.md
index 8c400bfd5c1..6f217864f50 100644
--- a/maps/feature-map/questions-and-approvals.md
+++ b/maps/feature-map/questions-and-approvals.md
@@ -28,7 +28,7 @@ Agent access: [companion](../agent-access-map/questions-and-approvals.md).
resolution. A visible request is not necessarily actionable by that viewer.
- `governance`: `questions-and-approvals/governance` — pending/history views, approve/reject, revision request, and
resubmission for formal board approvals.
-- `tool-review`: `questions-and-approvals/tool-review` — approve once, always allow the displayed scope, or decline;
+- `tool-review`: `questions-and-approvals/tool-review` — inspect the bounded server-redacted arguments and audit hash before approving once, always allowing the displayed scope, or declining;
decision status and provider execution outcome remain distinct.
- `continuation`: `questions-and-approvals/continuation` — one recorded response leads to the eligible waiting run or
continuation; repeated clicks, reloads, and competing tabs do not repeat work.
@@ -98,7 +98,7 @@ Use these checks alongside the entry-point recipes below and the operation recor
| `questions-and-approvals/plan-review` | Open the exact requested plan revision; reject one request and accept another. Inspect saved revision, work mode, owner, and the resulting continuation. | Edit after requesting review and reject stale acceptance. Test finalization readiness and bound completion review separately. In Agent Chat, inspect the later project/task creation and its source-plan provenance; acceptance alone is not a completed handoff. |
| `questions-and-approvals/audience` | Repeat a request for a named person, named agent, human-only resolver, and independent reviewer. Inspect actionability and final attribution. | Test creator exclusion, wrong audience/company, private task, missing or cancelled run, and changed responsible-user access. A human comment used by an agent remains subject to that agent’s resolver authority. |
| `questions-and-approvals/governance` | Create and inspect a formal request, comment, request revision, resubmit, then approve or reject distinct requests. Reload history and linked task state. | Test board decision authority separately from agent request/comment/resubmit authority. A repeated decision cannot repeat activation. Current pending status, payload revision, and governed side effects must agree. |
-| `questions-and-approvals/tool-review` | Invoke a harmless Ask first action, compare task and Apps review surfaces, then exercise Approve once, Always allow, and Decline separately. | No provider call before approval. Repeat decisions and compare invocation count. Change the tool definition, revoke access, or set Off before a decision. Record the approval decision and provider outcome independently. |
+| `questions-and-approvals/tool-review` | Invoke a harmless Ask first action with a proposal longer than its preview. Open Arguments and audit hash on task and Apps review surfaces and inspect the tail before a decision. Then exercise Approve once, Always allow, and Decline separately. | No provider call before approval. Repeat decisions and compare invocation count. Change the tool definition, revoke access, or set Off before a decision. Record the approval decision and provider outcome independently. |
| `questions-and-approvals/continuation` | After answering, inspect the saved response, delivery receipt, eligible run, and useful result in the original task/thread. | Reload, race two tabs, and interrupt delivery after saving. Require one continuation without repeated external actions. Reassignment, Stop, pause, budget, and external-thread boundaries remain effective. |
### `task-thread`
diff --git a/ui/src/components/IssueThreadInteractionCard.test.tsx b/ui/src/components/IssueThreadInteractionCard.test.tsx
index fd1953ac26b..25f6849efe3 100644
--- a/ui/src/components/IssueThreadInteractionCard.test.tsx
+++ b/ui/src/components/IssueThreadInteractionCard.test.tsx
@@ -1263,7 +1263,7 @@ describe("IssueThreadInteractionCard", () => {
});
describe("IssueThreadInteractionCard tool-action card", () => {
- it("shows only the request description, app icon, and decision controls", () => {
+ it("keeps the request description and decisions primary with optional argument inspection", () => {
const host = renderCard({ interaction: pendingToolActionWriteInteraction, onAcceptInteraction: vi.fn(), onRejectInteraction: vi.fn() });
expect(host.textContent).toContain("Add 1 row");
expect(host.querySelector('[role="img"]')?.getAttribute("aria-label")).toBe("Google Sheets");
@@ -1282,6 +1282,52 @@ describe("IssueThreadInteractionCard tool-action card", () => {
expect(host.textContent).toContain("Launch checklist");
});
+ it("renders only the server-redacted proposal as plain text when inspected", async () => {
+ const interaction = structuredClone(pendingToolActionWriteInteraction);
+ interaction.payload.toolAction!.argumentsSummaryJson = JSON.stringify({ text: ' {payload.argumentsHash}', apiKey: "[redacted]" });
+ const accept = vi.fn();
+ const host = renderCard({ interaction, onAcceptInteraction: accept });
+ const details = host.querySelector("details")!;
+ expect(details.open).toBe(false);
+ await act(async () => details.querySelector("summary")!.click());
+ expect(details.open).toBe(true);
+ expect(details.querySelector("pre")?.textContent).toBe(interaction.payload.toolAction!.argumentsSummaryJson);
+ expect(details.querySelector("img")).toBeNull();
+ expect(details.textContent).toContain("[redacted]");
+ expect(accept).not.toHaveBeenCalled();
+ });
+
+ it.each([undefined, { secret: "must not render" }, "x".repeat(20_001)])("withholds missing, malformed or oversized proposal details (%#)", summary => {
+ const interaction = structuredClone(pendingToolActionWriteInteraction);
+ Object.assign(interaction.payload.toolAction!, { argumentsSummaryJson: summary });
+ const host = renderCard({ interaction });
+ expect(host.querySelector("details")).toBeNull();
+ expect(host.textContent).toContain("Request arguments are unavailable.");
+ expect(host.textContent).not.toContain("must not render");
+ expect(host.querySelector("pre")).toBeNull();
+ expect(Array.from(host.querySelectorAll("button")).find(button => button.textContent === "Approve & run")?.disabled).toBe(true);
+ });
+
+ it.each(["argumentsSummaryJson", "argumentsHash"] as const)("rejects oversized whitespace before trimming %s", field => {
+ const interaction = structuredClone(pendingToolActionWriteInteraction);
+ const oversized = " ".repeat(field === "argumentsSummaryJson" ? 20_001 : 256);
+ interaction.payload.toolAction![field] = oversized;
+ const trim = String.prototype.trim;
+ let oversizedTrimCalls = 0;
+ const spy = vi.spyOn(String.prototype, "trim").mockImplementation(function (this: string) {
+ if (String(this) === oversized) oversizedTrimCalls++;
+ return trim.call(this);
+ });
+ try {
+ const host = renderCard({ interaction });
+ expect(host.querySelector("details")).toBeNull();
+ expect(host.textContent).toContain("Request arguments are unavailable.");
+ expect(oversizedTrimCalls).toBe(0);
+ } finally {
+ spy.mockRestore();
+ }
+ });
+
it("keeps the destructive request warning and approval styling", () => {
const host = renderCard({ interaction: pendingToolActionDestructiveInteraction, onAcceptInteraction: vi.fn(), onRejectInteraction: vi.fn() });
expect(host.textContent).toContain("cannot be undone");
diff --git a/ui/src/components/IssueThreadInteractionCard.tsx b/ui/src/components/IssueThreadInteractionCard.tsx
index c3ba890e901..38281d787d1 100644
--- a/ui/src/components/IssueThreadInteractionCard.tsx
+++ b/ui/src/components/IssueThreadInteractionCard.tsx
@@ -1799,6 +1799,12 @@ function RequestToolActionCard({
const resolutionErrorMessage = useResolutionErrorMessage();
const isPending = state === "pending";
const variant = payload.risk === "destructive" ? "destructive" : "cta";
+ // This server projection is already redacted and bounded by the interaction schema.
+ const inspectableArguments = typeof payload.argumentsSummaryJson === "string"
+ && payload.argumentsSummaryJson.length <= 20_000 && payload.argumentsSummaryJson.trim().length > 0
+ && typeof payload.argumentsHash === "string" && payload.argumentsHash.length <= 255
+ && payload.argumentsHash.trim().length > 0;
+
useEffect(() => {
if (!isPending) setWorking(null);
@@ -1828,6 +1834,17 @@ function RequestToolActionCard({
{!isPending ?
+ Arguments and audit hash
+
+
+ {payload.argumentsSummaryJson}
+
+
Request arguments are unavailable.
: null} {isPending ? (