Skip to content

Commit 6613a76

Browse files
OrKoNDevtools-frontend LUCI CQ
authored andcommitted
Implement cross-origin restrictions
This CL disables the input if the context is from a different origin and shows a message that the user needs to start a new conversation. Fixed: 377227220 Change-Id: I122678e49ac18ad170240357da0d115cd28fd75c Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5987815 Commit-Queue: Alex Rudenko <[email protected]> Reviewed-by: Nikolay Vitkov <[email protected]>
1 parent 2386f3a commit 6613a76

File tree

9 files changed

+169
-30
lines changed

9 files changed

+169
-30
lines changed

front_end/panels/freestyler/AiAgent.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99

1010
import * as Freestyler from './freestyler.js';
1111

12-
const {AiAgent, ResponseType} = Freestyler;
12+
const {AiAgent, ResponseType, ConversationContext} = Freestyler;
1313

1414
class AiAgentMock extends AiAgent<unknown> {
1515
type = Freestyler.AgentType.FREESTYLER;
@@ -225,4 +225,55 @@ describeWithEnvironment('AiAgent', () => {
225225
]);
226226
});
227227
});
228+
229+
describe('ConversationContext', () => {
230+
function getTestContext(origin: string) {
231+
class TestContext extends ConversationContext<undefined> {
232+
override getOrigin(): string {
233+
return origin;
234+
}
235+
override getItem(): undefined {
236+
return undefined;
237+
}
238+
}
239+
return new TestContext();
240+
}
241+
it('checks context origins', () => {
242+
const tests = [
243+
{
244+
contextOrigin: 'https://google.test',
245+
agentOrigin: 'https://google.test',
246+
isAllowed: true,
247+
},
248+
{
249+
contextOrigin: 'https://google.test',
250+
agentOrigin: 'about:blank',
251+
isAllowed: false,
252+
},
253+
{
254+
contextOrigin: 'https://google.test',
255+
agentOrigin: 'https://www.google.test',
256+
isAllowed: false,
257+
},
258+
{
259+
contextOrigin: 'https://a.test',
260+
agentOrigin: 'https://b.test',
261+
isAllowed: false,
262+
},
263+
{
264+
contextOrigin: 'https://a.test',
265+
agentOrigin: 'file:///tmp',
266+
isAllowed: false,
267+
},
268+
{
269+
contextOrigin: 'https://a.test',
270+
agentOrigin: 'http://a.test',
271+
isAllowed: false,
272+
},
273+
];
274+
for (const test of tests) {
275+
assert.strictEqual(getTestContext(test.contextOrigin).isOriginAllowed(test.agentOrigin), test.isAllowed);
276+
}
277+
});
278+
});
228279
});

front_end/panels/freestyler/AiAgent.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ export abstract class AiAgent<T> {
151151
readonly #sessionId: string = crypto.randomUUID();
152152
#aidaClient: Host.AidaClient.AidaClient;
153153
#serverSideLoggingEnabled: boolean;
154-
#origin?: string;
155154
abstract readonly preamble: string;
156155
abstract readonly options: AidaRequestOptions;
157156
abstract readonly clientFeature: Host.AidaClient.ClientFeature;
@@ -163,6 +162,11 @@ export abstract class AiAgent<T> {
163162
* the history chuck it created
164163
*/
165164
#history = new Map<number, ResponseData[]>();
165+
/**
166+
* Might need to be part of history in case we allow chatting in
167+
* historical conversations.
168+
*/
169+
#origin?: string;
166170

167171
constructor(opts: AgentOptions) {
168172
this.#aidaClient = opts.aidaClient;

front_end/panels/freestyler/FreestylerPanel.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import * as Common from '../../core/common/common.js';
66
import * as Host from '../../core/host/host.js';
7+
import type * as Platform from '../../core/platform/platform.js';
78
import * as SDK from '../../core/sdk/sdk.js';
89
import * as Workspace from '../../models/workspace/workspace.js';
910
import {findMenuItemWithLabel, getMenu} from '../../testing/ContextMenuHelpers.js';
@@ -799,4 +800,42 @@ describeWithEnvironment('FreestylerPanel', () => {
799800
// We don't show the context menu if there are not entries
800801
assert.isUndefined(contextMenu);
801802
});
803+
describe('cross-origin', () => {
804+
it('blocks input on cross origin requests', async () => {
805+
const networkRequest = sinon.createStubInstance(SDK.NetworkRequest.NetworkRequest, {
806+
url: 'https://a.test' as Platform.DevToolsPath.UrlString,
807+
});
808+
UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, networkRequest);
809+
panel = new Freestyler.FreestylerPanel(mockView, {
810+
aidaClient: getTestAidaClient(),
811+
aidaAvailability: Host.AidaClient.AidaAccessPreconditions.AVAILABLE,
812+
syncInfo: getTestSyncInfo(),
813+
});
814+
panel.markAsRoot();
815+
panel.show(document.body);
816+
817+
sinon.assert.calledWith(mockView, sinon.match({
818+
selectedNetworkRequest: new Freestyler.RequestContext(networkRequest),
819+
blockedByCrossOrigin: false,
820+
}));
821+
822+
// Send a query for https://a.test.
823+
panel.handleAction('drjones.network-floating-button');
824+
mockView.lastCall.args[0].onTextSubmit('test');
825+
await drainMicroTasks();
826+
827+
// Change context to https://b.test.
828+
const networkRequest2 = sinon.createStubInstance(SDK.NetworkRequest.NetworkRequest, {
829+
url: 'https://b.test' as Platform.DevToolsPath.UrlString,
830+
});
831+
UI.Context.Context.instance().setFlavor(SDK.NetworkRequest.NetworkRequest, networkRequest2);
832+
panel.handleAction('drjones.network-floating-button');
833+
await drainMicroTasks();
834+
835+
sinon.assert.calledWith(mockView, sinon.match({
836+
selectedNetworkRequest: new Freestyler.RequestContext(networkRequest2),
837+
blockedByCrossOrigin: true,
838+
}));
839+
});
840+
});
802841
});

front_end/panels/freestyler/FreestylerPanel.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
216216
selectedFile: null,
217217
selectedNetworkRequest: null,
218218
selectedAiCallTree: null,
219+
blockedByCrossOrigin: false,
219220
};
220221
}
221222

@@ -433,7 +434,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
433434
}
434435

435436
this.#viewProps.selectedElement = createNodeContext(selectedElementFilter(ev.data));
436-
this.doUpdate();
437+
this.#onContextSelectionChanged();
437438
};
438439

439440
#handleNetworkRequestFlavorChange =
@@ -443,7 +444,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
443444
}
444445

445446
this.#viewProps.selectedNetworkRequest = Boolean(ev.data) ? new RequestContext(ev.data) : null;
446-
this.doUpdate();
447+
this.#onContextSelectionChanged();
447448
};
448449

449450
#handleTraceEntryNodeFlavorChange =
@@ -453,7 +454,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
453454
}
454455

455456
this.#viewProps.selectedAiCallTree = Boolean(ev.data) ? new CallTreeContext(ev.data) : null;
456-
this.doUpdate();
457+
this.#onContextSelectionChanged();
457458
};
458459

459460
#handleUISourceCodeFlavorChange =
@@ -463,7 +464,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
463464
}
464465

465466
this.#viewProps.selectedFile = Boolean(ev.data) ? new FileContext(ev.data) : null;
466-
this.doUpdate();
467+
this.#onContextSelectionChanged();
467468
};
468469

469470
#handleFreestylerEnabledSettingChanged = (): void => {
@@ -568,6 +569,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
568569
this.#viewOutput.freestylerChatUi?.focusTextInput();
569570
Host.userMetrics.actionTaken(Host.UserMetrics.Action.FreestylerOpenedFromElementsPanelFloatingButton);
570571
this.#viewProps.messages = [];
572+
this.#onContextSelectionChanged();
571573
this.doUpdate();
572574
void this.#doConversation(this.#currentAgent.runFromHistory());
573575
}
@@ -618,6 +620,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
618620
}
619621
this.#viewProps.messages = [];
620622
this.#viewProps.agentType = undefined;
623+
this.#onContextSelectionChanged();
621624
this.doUpdate();
622625
}
623626

@@ -633,6 +636,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
633636
this.#viewProps.isLoading = false;
634637
if (this.#currentAgent) {
635638
this.#currentAgent = this.#createAgent(this.#currentAgent.type);
639+
this.#onContextSelectionChanged();
636640
}
637641
this.#cancel();
638642
this.doUpdate();
@@ -646,12 +650,26 @@ export class FreestylerPanel extends UI.Panel.Panel {
646650
this.doUpdate();
647651
}
648652

649-
async #startConversation(text: string): Promise<void> {
653+
#onContextSelectionChanged(): void {
650654
if (!this.#currentAgent) {
655+
this.#viewProps.blockedByCrossOrigin = false;
656+
this.doUpdate();
651657
return;
652658
}
653-
this.#runAbortController = new AbortController();
654-
const signal = this.#runAbortController.signal;
659+
const currentContext = this.#getConversationContext();
660+
if (!currentContext) {
661+
this.#viewProps.blockedByCrossOrigin = false;
662+
this.doUpdate();
663+
return;
664+
}
665+
this.#viewProps.blockedByCrossOrigin = !currentContext.isOriginAllowed(this.#currentAgent.origin);
666+
this.doUpdate();
667+
}
668+
669+
#getConversationContext(): ConversationContext<unknown>|null {
670+
if (!this.#currentAgent) {
671+
return null;
672+
}
655673
let context: ConversationContext<unknown>|null;
656674
switch (this.#currentAgent.type) {
657675
case AgentType.FREESTYLER:
@@ -667,10 +685,21 @@ export class FreestylerPanel extends UI.Panel.Panel {
667685
context = this.#viewProps.selectedAiCallTree;
668686
break;
669687
}
688+
return context;
689+
}
690+
691+
async #startConversation(text: string): Promise<void> {
692+
if (!this.#currentAgent) {
693+
return;
694+
}
695+
this.#runAbortController = new AbortController();
696+
const signal = this.#runAbortController.signal;
697+
const context = this.#getConversationContext();
670698
// If a different context is provided, it must be from the same origin.
671699
if (context && !context.isOriginAllowed(this.#currentAgent.origin)) {
672-
// TODO: inform the user here.
673-
// throw new Error('cross-origin context data should not be included');
700+
// This error should not be reached. If it happens, some
701+
// invariants do not hold anymore.
702+
throw new Error('cross-origin context data should not be included');
674703
}
675704
const runner = this.#currentAgent.run(text, {
676705
signal,

front_end/panels/freestyler/components/FreestylerChatUi.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ css
6060
isLoading: false,
6161
canShowFeedbackForm: false,
6262
userInfo: {},
63+
blockedByCrossOrigin: false,
6364
...options,
6465
};
6566
}

front_end/panels/freestyler/components/FreestylerChatUi.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ const UIStringsNotTranslate = {
115115
*@description Placeholder text for the chat UI input.
116116
*/
117117
inputPlaceholderForDrJonesPerformanceAgent: 'Ask a question about the selected item and its call tree',
118+
/**
119+
* @description Placeholder text for the input shown when the conversation is blocked because a cross-origin context was selected.
120+
*/
121+
crossOriginError: 'To talk about data from another origin, start a new chat',
118122
/**
119123
*@description Title for the send icon button.
120124
*/
@@ -246,22 +250,6 @@ const str_ = i18n.i18n.registerUIStrings('panels/freestyler/components/Freestyle
246250
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
247251
const lockedString = i18n.i18n.lockedString;
248252

249-
function getInputPlaceholderString(state: State, agentType?: AgentType): Platform.UIString.LocalizedString {
250-
if (state === State.CONSENT_VIEW || !agentType) {
251-
return i18nString(UIStrings.followTheSteps);
252-
}
253-
switch (agentType) {
254-
case AgentType.FREESTYLER:
255-
return lockedString(UIStringsNotTranslate.inputPlaceholderForFreestylerAgent);
256-
case AgentType.DRJONES_FILE:
257-
return lockedString(UIStringsNotTranslate.inputPlaceholderForDrJonesFileAgent);
258-
case AgentType.DRJONES_NETWORK_REQUEST:
259-
return lockedString(UIStringsNotTranslate.inputPlaceholderForDrJonesNetworkAgent);
260-
case AgentType.DRJONES_PERFORMANCE:
261-
return lockedString(UIStringsNotTranslate.inputPlaceholderForDrJonesPerformanceAgent);
262-
}
263-
}
264-
265253
export interface Step {
266254
isLoading: boolean;
267255
thought?: string;
@@ -321,6 +309,7 @@ export interface Props {
321309
canShowFeedbackForm: boolean;
322310
userInfo: Pick<Host.InspectorFrontendHostAPI.SyncInformation, 'accountImage'|'accountFullName'>;
323311
agentType?: AgentType;
312+
blockedByCrossOrigin: boolean;
324313
}
325314

326315
// The model returns multiline code blocks in an erroneous way with the language being in new line.
@@ -408,6 +397,9 @@ export class FreestylerChatUi extends HTMLElement {
408397
}
409398

410399
#isTextInputDisabled = (): boolean => {
400+
if (this.#props.blockedByCrossOrigin) {
401+
return true;
402+
}
411403
const isAidaAvailable = this.#props.aidaAvailability === Host.AidaClient.AidaAccessPreconditions.AVAILABLE;
412404
const isConsentView = this.#props.state === State.CONSENT_VIEW;
413405
const showsSideEffects = this.#props.messages.some(message => {
@@ -867,7 +859,7 @@ export class FreestylerChatUi extends HTMLElement {
867859
<div class=${resourceClass}>${
868860
this.#props.selectedElement
869861
? LitHtml.Directives.until(
870-
Common.Linkifier.Linkifier.linkify(this.#props.selectedElement),
862+
Common.Linkifier.Linkifier.linkify(this.#props.selectedElement.getItem()),
871863
)
872864
: html`<span>${
873865
lockedString(UIStringsNotTranslate.noElementSelected)
@@ -994,6 +986,27 @@ export class FreestylerChatUi extends HTMLElement {
994986
}
995987
};
996988

989+
#getInputPlaceholderString(): Platform.UIString.LocalizedString {
990+
const state = this.#props.state;
991+
const agentType = this.#props.agentType;
992+
if (state === State.CONSENT_VIEW || !agentType) {
993+
return i18nString(UIStrings.followTheSteps);
994+
}
995+
if (this.#props.blockedByCrossOrigin) {
996+
return lockedString(UIStringsNotTranslate.crossOriginError);
997+
}
998+
switch (agentType) {
999+
case AgentType.FREESTYLER:
1000+
return lockedString(UIStringsNotTranslate.inputPlaceholderForFreestylerAgent);
1001+
case AgentType.DRJONES_FILE:
1002+
return lockedString(UIStringsNotTranslate.inputPlaceholderForDrJonesFileAgent);
1003+
case AgentType.DRJONES_NETWORK_REQUEST:
1004+
return lockedString(UIStringsNotTranslate.inputPlaceholderForDrJonesNetworkAgent);
1005+
case AgentType.DRJONES_PERFORMANCE:
1006+
return lockedString(UIStringsNotTranslate.inputPlaceholderForDrJonesPerformanceAgent);
1007+
}
1008+
}
1009+
9971010
#renderChatInput = (): LitHtml.TemplateResult => {
9981011
// clang-format off
9991012
return html`
@@ -1002,7 +1015,7 @@ export class FreestylerChatUi extends HTMLElement {
10021015
.disabled=${this.#isTextInputDisabled()}
10031016
wrap="hard"
10041017
@keydown=${this.#handleTextAreaKeyDown}
1005-
placeholder=${getInputPlaceholderString(this.#props.state, this.#props.agentType)}
1018+
placeholder=${this.#getInputPlaceholderString()}
10061019
jslog=${VisualLogging.textField('query').track({ keydown: 'Enter' })}></textarea>
10071020
${this.#props.isLoading
10081021
? html`<devtools-button

front_end/panels/freestyler/components/freestylerChatUi.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
}
102102

103103
&:disabled {
104-
color: var(--sys-color-state-disabled);
104+
color: var(--sys-color-on-surface-subtle);
105105
background-color: var(--sys-color-state-disabled-container);
106106
border-color: transparent;
107107
}

front_end/ui/components/docs/freestyler/basic.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const component = new Freestyler.FreestylerChatUi({
5959
isLoading: false,
6060
canShowFeedbackForm: false,
6161
userInfo: {},
62+
blockedByCrossOrigin: false,
6263
});
6364

6465
document.getElementById('container')?.appendChild(component);

front_end/ui/components/docs/freestyler/empty_state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const component = new Freestyler.FreestylerChatUi({
3434
isLoading: false,
3535
canShowFeedbackForm: false,
3636
userInfo: {},
37+
blockedByCrossOrigin: false,
3738
});
3839

3940
document.getElementById('container')?.appendChild(component);

0 commit comments

Comments
 (0)