Skip to content
Closed
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
34 changes: 34 additions & 0 deletions docs/audits/pr-2381/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Approval argument labels: PR #2381 repair

The branch was rebased on main before repairing Rhea's HOLD (review 5598440568).
`LABEL_KEYS` now accepts own entries only. Canonical JSON argument names such as
`toString`, `constructor`, `hasOwnProperty` and `__proto__` use their raw names,
just like the ordinary unknown-key control `extra`. The same own-key rule
protects the tool-name verb table.

The new helper cases failed for all four inherited argument names before the
fix. Afterward, helper and real approval-card suites pass 40 tests. Card tests
use the configured i18next resources, fetch the pinned envelope through the
API seam, preserve the exact JSON disclosure, and verify Approve and Decline
remain enabled. Frontend typecheck and changed-file lint pass.

## Local browser evidence

`frontend/design-system/preview/approval-readable.html` renders the actual
rebased approval component, shell CSS and self-hosted fonts. Isolated headless
Chrome intercepts the pending-approval GET with controlled merge/create-issue
envelopes; no vendor execution, live account, approval or production API was
used. The ego command was refused by the seat wrapper, so the check uses a
fresh local Chrome profile.

The en/zh-CN captures at 1440 and 390 show all inherited-key rows. Opening the
second card's disclosure preserves every JSON own key. Both consent controls
remain usable, with zero page errors, zero POSTs and no horizontal overflow.
`browser-proof.json` records the measurements. Existing base button heights
remain 33px in en and 34px in zh-CN, including at 390; this repair does not
claim a new mobile target-size gate.

These are local component fixtures, not a deployed consent flow or a vendor
execution. Current-head code/security and UX re-gates remain required.

The refreshed proof verifies IBM Plex Sans/Mono loaded and records zero HTTP error responses. The local server explicitly allowed the real dependency directory behind the worktree symlink; earlier captures without loaded fonts are superseded.
72 changes: 72 additions & 0 deletions docs/audits/pr-2381/browser-proof.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"source": "actual rebased components; local controlled API fixtures, not deployed or vendor calls",
"proof": [
{
"lang": "en",
"width": 1440,
"overflow": false,
"approveHeights": [
33,
33
],
"allOwnKeysVisible": true,
"exactJsonPreserved": true,
"loadedFonts": [
"IBM Plex Sans",
"IBM Plex Sans",
"IBM Plex Mono"
]
},
{
"lang": "en",
"width": 390,
"overflow": false,
"approveHeights": [
33,
33
],
"allOwnKeysVisible": true,
"exactJsonPreserved": true,
"loadedFonts": [
"IBM Plex Sans",
"IBM Plex Sans",
"IBM Plex Mono"
]
},
{
"lang": "zh-CN",
"width": 1440,
"overflow": false,
"approveHeights": [
34,
34
],
"allOwnKeysVisible": true,
"exactJsonPreserved": true,
"loadedFonts": [
"IBM Plex Sans",
"IBM Plex Sans",
"IBM Plex Mono"
]
},
{
"lang": "zh-CN",
"width": 390,
"overflow": false,
"approveHeights": [
34,
34
],
"allOwnKeysVisible": true,
"exactJsonPreserved": true,
"loadedFonts": [
"IBM Plex Sans",
"IBM Plex Sans",
"IBM Plex Mono"
]
}
],
"errors": [],
"posts": 0,
"failedResponses": []
}
Binary file added docs/audits/pr-2381/en-1440.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/audits/pr-2381/en-390.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/audits/pr-2381/zh-CN-1440.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/audits/pr-2381/zh-CN-390.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/design-system/preview/approval-readable.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!doctype html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Approval arguments specimen</title></head><body style="margin:0"><div id="root"></div><script type="module" src="./approval-readable.tsx"></script></body></html>
27 changes: 27 additions & 0 deletions frontend/design-system/preview/approval-readable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { MemoryRouter } from 'react-router-dom';
import { AuthContext } from '../../src/context/AuthContext';
import i18n, { i18nReady } from '../../src/i18n';
import V2ApprovalCard from '../../src/v2/components/V2ApprovalCard';
import '../../src/v2/v2.css';
import '@fontsource/ibm-plex-sans/400.css';
import '@fontsource/ibm-plex-sans/600.css';
import '@fontsource/ibm-plex-mono/500.css';

async function show() {
await i18nReady;
await i18n.changeLanguage(new URLSearchParams(location.search).get('lang') || 'en');
createRoot(document.getElementById('root')!).render(
<AuthContext.Provider value={{currentUser:{_id:'viewer-1'},token:null} as never}>
<MemoryRouter><div className="v2-root" style={{height:'auto',minHeight:'100vh',padding:16,boxSizing:'border-box'}}>
<main style={{maxWidth:560,margin:'0 auto',display:'grid',gap:24}}>
{['merge','inherited'].map(id => <V2ApprovalCard key={id} authorLabel="Scout" time="now" message={{
id, pod_id:'pod-1', content:'Fixture approval', payload:{kind:'approval-card',approvalId:id,status:'flagged',ownerUserId:'viewer-1',actionType:'tool_call'},
} as never} />)}
</main>
</div></MemoryRouter>
</AuthContext.Provider>,
);
}
void show();
21 changes: 20 additions & 1 deletion frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1380,7 +1380,26 @@
"failed": "Approved — tool call failed ({{code}}).",
"refused": "Approved — tool call refused ({{code}}).",
"pinChanged": "Approved, but the tool changed since this request. Ask again before running it.",
"expiredPending": "This tool-call approval has expired."
"expiredPending": "This tool-call approval has expired.",
"verb": {
"mergePullRequest": "Merge pull request #{{number}}",
"createIssue": "Open an issue",
"commentOnIssue": "Comment on issue #{{number}}",
"closeIssue": "Close issue #{{number}}"
},
"label": {
"repository": "Repository",
"pullRequest": "Pull request",
"issue": "Issue",
"head": "Head commit",
"mergeMethod": "Merge method",
"commitTitle": "Commit title",
"commitMessage": "Commit message",
"title": "Title",
"body": "Body",
"labels": "Labels"
},
"exactArgs": "Exact arguments"
},
"approve": "Approve",
"decline": "Decline",
Expand Down
23 changes: 21 additions & 2 deletions frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1372,12 +1372,31 @@
"failed": "已批准 — 工具调用失败({{code}})。",
"refused": "已批准 — 工具调用被拒绝({{code}})。",
"pinChanged": "已批准,但工具在请求后已变更。请重新发起请求后再执行。",
"expiredPending": "该工具调用批准已过期。"
"expiredPending": "该工具调用批准已过期。",
"verb": {
"mergePullRequest": "合并 PR #{{number}}",
"createIssue": "创建 Issue",
"commentOnIssue": "评论 Issue #{{number}}",
"closeIssue": "关闭 Issue #{{number}}"
},
"label": {
"repository": "仓库",
"pullRequest": "PR",
"issue": "Issue",
"head": "头提交",
"mergeMethod": "合并方式",
"commitTitle": "提交标题",
"commitMessage": "提交信息",
"title": "标题",
"body": "正文",
"labels": "标签"
},
"exactArgs": "原始参数"
},
"approve": "批准",
"decline": "拒绝",
"deciding": "处理中…",
"waitingForOwner": "等待工作区所有者决定。",
"waitingForOwner": "等待工作空间所有者决定。",
"aged": "该提议已等待较久 — 请确认它仍然适用。",
"approved": "✓ 已批准 — 已完成。",
"approvedButFailed": "已批准,但执行失败:{{error}}",
Expand Down
81 changes: 80 additions & 1 deletion frontend/src/v2/__tests__/V2ApprovalCardAction.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { MemoryRouter, useLocation } from 'react-router-dom';
import V2ApprovalCard from '../components/V2ApprovalCard';
import en from '../../i18n/locales/en.json';
Expand Down Expand Up @@ -279,6 +279,50 @@ describe('tool-call approval details stay owner-scoped', () => {
expect(mockGet).toHaveBeenCalledWith('/api/approvals/pending?podId=pod-1');
});

test('the owner reads the call as a verb and rows, with the exact arguments one click away', async () => {
const headSha = 'abc1234def5678abc1234def5678abc1234def56';
mockGet.mockResolvedValue({
approvals: [{
approvalId: 'tool-2',
messageId: 'm1',
toolCall: {
tool: 'github.merge_pull_request',
canonicalArgs: { pullNumber: 4, mergeMethod: 'squash', headSha, owner: 'Team-Commonly', repo: 'demo-day' },
},
}],
});

renderCard({
kind: 'approval-card',
approvalId: 'tool-2',
status: 'flagged',
ownerUserId: 'viewer-1',
actionType: 'tool_call',
summary: 'Merge the pull request',
});

await waitFor(() => expect(screen.getByTestId('approval-args')).toBeInTheDocument());
const action = screen.getByTestId('approval-action');
expect(action).toHaveTextContent('Merge pull request #4');
expect(action).toHaveTextContent('github.merge_pull_request');

const repoLink = within(screen.getByTestId('approval-arg-owner-repo')).getByRole('link', { name: 'Team-Commonly/demo-day' });
expect(repoLink).toHaveAttribute('href', 'https://github.com/Team-Commonly/demo-day');
expect(repoLink).toHaveAttribute('rel', 'noopener noreferrer');
expect(within(screen.getByTestId('approval-arg-pullNumber')).getByRole('link', { name: '#4' }))
.toHaveAttribute('href', 'https://github.com/Team-Commonly/demo-day/pull/4');
const head = screen.getByTestId('approval-arg-headSha');
expect(head).toHaveTextContent(en.approvalCard.toolCall.label.head);
expect(head).toHaveTextContent('abc1234');
expect(head.querySelector('dd')).toHaveAttribute('title', headSha);
expect(screen.getByTestId('approval-arg-mergeMethod')).toHaveTextContent('squash');

// The exact JSON is still on the card, behind a disclosure, never dropped.
expect(screen.getByText(en.approvalCard.toolCall.exactArgs).closest('details')).not.toBeNull();
expect(screen.getByTestId('approval-tool-call')).toHaveTextContent(`"headSha": "${headSha}"`);
expect(screen.getByText(en.approvalCard.approve)).toBeEnabled();
});

test('a non-owner never fetches or renders canonical args', () => {
renderCard({
kind: 'approval-card',
Expand Down Expand Up @@ -330,3 +374,38 @@ describe('tool-call approval details stay owner-scoped', () => {
expect(screen.getByText(en.approvalCard.decline)).toBeEnabled();
});
});

test.each(['toString', 'constructor', 'hasOwnProperty', '__proto__', 'extra'])('canonical argument %s keeps the real consent card usable', async key => {
const canonicalArgs = JSON.parse(JSON.stringify({ title:'Pinned issue', [key]:'pinned-not-prose' }));
mockGet.mockResolvedValue({approvals:[{approvalId:'inherited-key',messageId:'m1',toolCall:{tool:'github.create_issue',canonicalArgs}}]});
renderCard({kind:'approval-card',approvalId:'inherited-key',status:'flagged',ownerUserId:'viewer-1',actionType:'tool_call'});
const row = await screen.findByTestId(`approval-arg-${key}`);
expect(within(row).getByText(key)).toBeInTheDocument();
expect(within(row).getByText('pinned-not-prose')).toBeInTheDocument();
expect(screen.getByTestId('approval-tool-call')).toHaveTextContent(`"${key}": "pinned-not-prose"`);
expect(screen.getByText(en.approvalCard.approve)).toBeEnabled();
expect(screen.getByText(en.approvalCard.decline)).toBeEnabled();
});


test.each([
['github.merge_pull_request', { pullNumber: 42, issueNumber: 7 }, 'Merge pull request #42'],
['github.comment_on_issue', { pullNumber: 7, issueNumber: 42 }, 'Comment on issue #42'],
['github.close_issue', { pullNumber: 7, issueNumber: 42 }, 'Close issue #42'],
['github.merge_pull_request', { pullNumber: 0, issueNumber: 7 }, null],
['github.comment_on_issue', { pullNumber: 7, issueNumber: 0 }, null],
['github.close_issue', { pullNumber: 7, issueNumber: 0 }, null],
])('consent for %s uses only its required number (%j)', async (tool, canonicalArgs, expected) => {
mockGet.mockResolvedValue({ approvals: [{
approvalId: 'number-selection', messageId: 'm1', toolCall: { tool, canonicalArgs },
}] });
renderCard({ kind: 'approval-card', approvalId: 'number-selection', status: 'flagged',
ownerUserId: 'viewer-1', actionType: 'tool_call' });
await waitFor(() => expect(screen.getByTestId('approval-args')).toBeInTheDocument());
const action = screen.getByTestId('approval-action');
expect(action).toHaveTextContent(tool);
if (expected) expect(action).toHaveTextContent(expected);
else expect(action).not.toHaveTextContent(/#7/);
expect(screen.getByTestId('approval-arg-pullNumber')).toBeInTheDocument();
expect(screen.getByTestId('approval-arg-issueNumber')).toBeInTheDocument();
});
Loading
Loading