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
3 changes: 3 additions & 0 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2794,6 +2794,9 @@
"connectionSeparator": " · ",
"connectionSignedIn": "Signed in",
"connectionKey": "Key",
"keyOnlyAgent": "Only {{agent}} can use this key.",
"keyAgentNotInPod": "{{agent}} isn't in this pod.",
"keyAgentNotInPodUnnamed": "This key's agent isn't in this pod.",
"disconnectConnectionLabel": "Disconnect {{label}}: {{connection}}",
"operatorKey": {
"note": "add a key for one of your agents",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2795,6 +2795,9 @@
"connectionSeparator": " · ",
"connectionSignedIn": "已登录",
"connectionKey": "密钥",
"keyOnlyAgent": "只有 {{agent}} 可以使用这个密钥。",
"keyAgentNotInPod": "{{agent}} 不在这个 Pod 中。",
"keyAgentNotInPodUnnamed": "密钥所属的智能体不在这个 Pod 中。",
"disconnectConnectionLabel": "断开 {{label}}:{{connection}}",
"operatorKey": {
"note": "为你的智能体添加密钥",
Expand Down
165 changes: 164 additions & 1 deletion frontend/src/v2/__tests__/ConnectorsPage.operatorKey.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,28 @@ describe('Connectors page: operator-key entries', () => {
// Scout's key is granted; Reed has none yet, so the form offers Reed only.
expect(within(drawer).getByLabelText('key')).toBeInTheDocument();
expect(agentOptions(drawer)).toEqual(['Reed']);
// Where to make the key sits beside the form here too.
expect(within(drawer).getByText('Create a key with read scopes only.')).toBeInTheDocument();
expect(within(drawer).getByRole('link', { name: 'Create a key at Latchkey' })).toHaveAttribute('href', 'https://latchkey.example/keys');
// The grant row still renders beside the form.
expect(drawer.querySelector('.v2-cx-list article')).not.toBeNull();
});

it('an agent installed only in an agent-dm pod is not offered, and an agent-dm install by someone else does not block', async () => {
axios.post.mockImplementation(async () => ({ data: {} }));
const dmPods = [...pods, { _id: 'dm1', name: 'Scout ↔ Nova', type: 'agent-dm', members: [{ _id: 'u1', username: 'sam' }] }];
mockApi({
pods: dmPods,
// Nova is the viewer's install only in the agent-dm pod; Reed is the
// viewer's in p2, and someone else's only in the agent-dm pod.
seats: { p1: [scout], p2: [reed], dm1: [{ ...nova, installedBy: 'u1' }, { ...reed, installedBy: 'u2' }] },
grants: [],
route: (url) => (url === '/api/installables' ? Promise.resolve({ data: { installables: [keyedEntry] } }) : undefined),
});
renderPage();

const popover = await openToolPopover('Latchkey');
expect(agentOptions(popover)).toEqual(['Reed', 'Scout']);
});

it('a success without the route\'s connected receipt reads as a failure, and does not refresh', async () => {
Expand Down Expand Up @@ -282,7 +304,8 @@ describe('Connectors page: operator-key entries', () => {
});

it('with one agent keyed, a key can still be added for another, and the keyed one is named', async () => {
mockCatalog([{ ...keyedEntry, connections: [scoutKey] }]);
const reedKey = { ...scoutKey, connectionId: 'conn-reed', agentId: REED };
mockCatalog([{ ...keyedEntry, connections: [scoutKey] }], [{ ...keyedEntry, connections: [scoutKey, reedKey] }]);
renderPage();

const popover = await openToolPopover('Latchkey');
Expand All @@ -294,6 +317,10 @@ describe('Connectors page: operator-key entries', () => {
paste(popover);
fireEvent.click(within(popover).getByRole('button', { name: 'Add key' }));
await waitFor(() => expect(axios.post).toHaveBeenCalledWith(OPERATOR_KEY_URL, { agentId: REED, key: KEY }, expect.anything()));
// After the refresh both keys show, and no agent is left to key.
expect(await screen.findByRole('button', { name: /^Disconnect Latchkey: Key · Reed · / })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /^Disconnect Latchkey: Key · Scout · / })).toBeInTheDocument();
expect(screen.queryByLabelText('key')).toBeNull();
});

it('every own agent keyed: no form; no own agent at all: a note instead', async () => {
Expand Down Expand Up @@ -332,3 +359,139 @@ describe('Connectors page: operator-key entries', () => {
}
});
});

// The grant draft on a key connection (#3353 review). The grant route refuses any
// audience on a key connection except exactly its agent, so the draft starts
// there, offers no one else, and names each key by its agent.
describe('Connectors page: granting an operator-key connection', () => {
const reedKey = { ...scoutKey, connectionId: 'conn-reed', agentId: REED };
const grantPost = () => axios.post.mock.calls.find(([url]) => url === '/api/grants');
const openKeyDraft = async (connections, { add = 'Add', form = 'Add Latchkey' } = {}) => {
axios.post.mockResolvedValue({ data: { grantId: 'grant_new' } });
mockApi({ pods, seats, grants: [], installables: [{ ...keyedEntry, connections }] });
renderPage();
const popover = await openToolPopover('Latchkey');
fireEvent.click(within(popover).getByRole('button', { name: add }));
return screen.findByRole('complementary', { name: form });
};
const optionTexts = (select) => within(select).getAllByRole('option').map((option) => option.textContent);
const keyGrant = (over) => ({
grantId: 'grant_k', installationId: 'conn-scout', target: { kind: 'pod', id: 'p1' }, tools: ['latchkey.list_secrets'],
writeMode: 'read', budget: { calls: 50, windowMs: 3600000 }, effectiveAudience: [SCOUT],
expiresAt: new Date(Date.now() + 6 * 86400000).toISOString(), revokedAt: null, revokedBy: null,
parentGrantId: null, rootGrantId: null, createdAt: new Date(Date.now() - 3600000).toISOString(), grantedBy: 'u1',
...over,
});
// A live grant on Scout's key keeps the tile granted, so it opens the drawer;
// `dead` is the revoked grant whose Grant again is under test.
const openGrantAgain = async (connections, dead) => {
axios.post.mockResolvedValue({ data: { grantId: 'grant_new' } });
mockApi({ pods, seats, grants: [keyGrant({}), dead], installables: [{ ...keyedEntry, connections }] });
renderPage();
const drawer = await openDrawer('Latchkey');
fireEvent.click(within(drawer).getByRole('button', { name: 'Grant again' }));
return screen.findByRole('complementary', { name: 'Add Latchkey' });
};
const revoked = { revokedAt: new Date(Date.now() - 600000).toISOString(), revokedBy: 'u1' };

it('Grant again on a key grant keeps the key it was minted on, even when it is not the first key', async () => {
const dead = keyGrant({ grantId: 'grant_reed', installationId: 'conn-reed', target: { kind: 'pod', id: 'p2' }, effectiveAudience: [REED], ...revoked });
const form = await openGrantAgain([scoutKey, reedKey], dead);
expect(within(form).getByRole('heading', { name: 'Latchkey · Ops' })).toBeInTheDocument();
fireEvent.click(within(form).getByRole('button', { name: 'Grant' }));
await waitFor(() => expect(grantPost()).toBeDefined());
expect(grantPost()[1]).toEqual(expect.objectContaining({ connectionId: 'conn-reed', target: { kind: 'pod', id: 'p2' }, audience: [REED] }));
});

it('Grant again on a key grant whose key is gone starts with nobody, never another agent\'s key', async () => {
const dead = keyGrant({ grantId: 'grant_gone', installationId: 'conn-deleted', target: { kind: 'pod', id: 'p2' }, effectiveAudience: [REED], ...revoked });
const form = await openGrantAgain([scoutKey, reedKey], dead);
expect(within(form).queryAllByRole('checkbox', { checked: true })).toHaveLength(0);
expect(within(form).getByRole('button', { name: 'Grant' })).toBeDisabled();
fireEvent.click(within(form).getByRole('button', { name: 'Grant' }));
expect(grantPost()).toBeUndefined();
});

it('in a pod with several agents, the first Grant reaches only the key\'s agent', async () => {
const form = await openKeyDraft([scoutKey, reedKey]);
// Launch pod holds Scout and Nova; only Scout's checkbox is offered.
expect(within(form).getByRole('heading', { name: 'Latchkey · Launch pod' })).toBeInTheDocument();
expect(within(form).getByRole('checkbox', { name: 'Scout' })).toBeChecked();
expect(within(form).queryByRole('checkbox', { name: 'Nova' })).toBeNull();
expect(within(form).getByText('Only Scout can use this key.')).toBeInTheDocument();
fireEvent.click(within(form).getByRole('button', { name: 'Grant' }));
await waitFor(() => expect(grantPost()).toBeDefined());
expect(grantPost()[1]).toEqual(expect.objectContaining({ connectionId: 'conn-scout', target: { kind: 'pod', id: 'p1' }, audience: [SCOUT] }));
});

it('the connection picker names each key by its agent, not "/"', async () => {
const form = await openKeyDraft([scoutKey, reedKey]);
const picker = within(form).getByLabelText('connection');
expect(optionTexts(picker)).toEqual(['Scout', 'Reed']);
expect(optionTexts(picker)).not.toContain('/');
});

it('a key whose agent is unknown on this page reads "Key"', async () => {
const form = await openKeyDraft([scoutKey, { ...reedKey, agentId: '6a00000000000000000000ff' }]);
expect(optionTexts(within(form).getByLabelText('connection'))).toEqual(['Scout', 'Key']);
});

it('switching to Reed\'s key in a pod Reed is not in says so and keeps Grant off; in Reed\'s pod it grants [Reed]', async () => {
const form = await openKeyDraft([scoutKey, reedKey]);
fireEvent.change(within(form).getByLabelText('connection'), { target: { value: 'conn-reed' } });
expect(within(form).getByText("Reed isn't in this pod.")).toBeInTheDocument();
expect(within(form).queryByRole('checkbox')).toBeNull();
expect(within(form).queryByText('Select at least one agent to grant access.')).toBeNull();
expect(within(form).getByRole('button', { name: 'Grant' })).toBeDisabled();

fireEvent.change(within(form).getByLabelText('pod'), { target: { value: 'p2' } });
expect(within(form).queryByText("Reed isn't in this pod.")).toBeNull();
// Ops holds Reed and Scout; Reed's key offers Reed alone.
expect(within(form).getByRole('checkbox', { name: 'Reed' })).toBeChecked();
expect(within(form).queryByRole('checkbox', { name: 'Scout' })).toBeNull();
fireEvent.click(within(form).getByRole('button', { name: 'Grant' }));
await waitFor(() => expect(grantPost()).toBeDefined());
expect(grantPost()[1]).toEqual(expect.objectContaining({ connectionId: 'conn-reed', target: { kind: 'pod', id: 'p2' }, audience: [REED] }));
});

it('a lone key starts in a pod where its agent sits', async () => {
const form = await openKeyDraft([reedKey]);
expect(within(form).getByRole('heading', { name: 'Latchkey · Ops' })).toBeInTheDocument();
expect(within(form).getByRole('checkbox', { name: 'Reed' })).toBeChecked();
});

it('a key whose agent sits in no pod starts with nobody and Grant off', async () => {
const form = await openKeyDraft([{ ...reedKey, agentId: '6a00000000000000000000ff' }]);
expect(within(form).getByText("This key's agent isn't in this pod.")).toBeInTheDocument();
expect(within(form).getByRole('button', { name: 'Grant' })).toBeDisabled();
});

it('an OAuth draft still starts with every agent in the pod', async () => {
axios.post.mockResolvedValue({ data: { grantId: 'grant_new' } });
const signIn = { connectionId: 'conn-linear', owner: '', repo: '', connectedAt: '2026-11-01T09:00:00.000Z' };
mockApi({ pods, seats, grants: [], installables: [{ ...linearEntry, connections: [signIn] }] });
renderPage();
const popover = await openToolPopover('Linear');
fireEvent.click(within(popover).getByRole('button', { name: 'Add' }));
const form = await screen.findByRole('complementary', { name: 'Add Linear' });
expect(within(form).getByRole('checkbox', { name: 'Scout' })).toBeChecked();
expect(within(form).getByRole('checkbox', { name: 'Nova' })).toBeChecked();
expect(within(form).queryByText(/can use this key/)).toBeNull();
fireEvent.click(within(form).getByRole('button', { name: 'Grant' }));
await waitFor(() => expect(grantPost()).toBeDefined());
expect(grantPost()[1].audience).toEqual([SCOUT, NOVA]);
});

it('reads in zh-CN', async () => {
await i18nReady;
await act(async () => { await i18n.changeLanguage('zh-CN'); });
try {
const form = await openKeyDraft([scoutKey, reedKey], { add: '添加', form: '添加 Latchkey' });
expect(within(form).getByText('只有 Scout 可以使用这个密钥。')).toBeInTheDocument();
fireEvent.change(form.querySelectorAll('select')[1], { target: { value: 'conn-reed' } });
expect(within(form).getByText('Reed 不在这个 Pod 中。')).toBeInTheDocument();
} finally {
await act(async () => { await i18n.changeLanguage('en'); });
}
});
});
8 changes: 6 additions & 2 deletions frontend/src/v2/connectors/ConnectorsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -461,13 +461,17 @@ const ConnectorsPage: React.FC = () => {
const entryGrants = grantsOf(toolEntry);
const liveGrants = entryGrants.filter((grant) => !isDead(grant, m.now));
const deadGrants = entryGrants.filter((grant) => isDead(grant, m.now));
const grantedKeyForm = toolGranted(toolEntry) ? tp.renderKeyForm(toolEntry) : null;
return (
<ConnectorDrawer notices={noticeBlock} type={toolEntry.installableId} title={toolEntry.label} onClose={closeTools}>
{/* Ungranted, the offer below carries the note through renderEntryNote. */}
{toolGranted(toolEntry) && tp.renderLinearWriteNote(toolEntry)}
{/* A granted operator-key entry still takes a key for an agent that has none. */}
{toolGranted(toolEntry) && tp.renderKeyForm(toolEntry) && (
<div className="v2-cx-pop__acts">{tp.renderKeyForm(toolEntry)}</div>
{grantedKeyForm && (
<>
{tp.renderKeyGuidance(toolEntry)}
<div className="v2-cx-pop__acts">{grantedKeyForm}</div>
</>
)}
{!toolGranted(toolEntry) && (
<div className="v2-cx-drawer__offer">
Expand Down
Loading
Loading