Skip to content

Commit bd344de

Browse files
OrKoNDevtools-frontend LUCI CQ
authored andcommitted
Pre-factoring for cross-origin restrictions
- introduces a context class for checking origins - makes the agent instance assign an origin to itself based on the context Bug: 377227220 Change-Id: If4b629d8bbf2427f0df35dd6807235459a257338 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5987650 Reviewed-by: Nikolay Vitkov <[email protected]> Commit-Queue: Alex Rudenko <[email protected]>
1 parent f31e4cc commit bd344de

15 files changed

+279
-112
lines changed

front_end/panels/freestyler/AiAgent.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,21 @@ export const enum AgentType {
127127

128128
const MAX_STEP = 10;
129129

130+
export abstract class ConversationContext<T> {
131+
abstract getOrigin(): string;
132+
abstract getItem(): T;
133+
isOriginAllowed(agentOrigin: string|undefined): boolean {
134+
if (!agentOrigin) {
135+
return true;
136+
}
137+
// Currently does not handle opaque origins because they
138+
// are not available to DevTools, instead checks
139+
// that serialization of the origin is the same
140+
// https://html.spec.whatwg.org/#ascii-serialisation-of-an-origin.
141+
return this.getOrigin() === agentOrigin;
142+
}
143+
}
144+
130145
export abstract class AiAgent<T> {
131146
static validTemperature(temperature: number|undefined): number|undefined {
132147
return typeof temperature === 'number' && temperature >= 0 ? temperature : undefined;
@@ -136,11 +151,12 @@ export abstract class AiAgent<T> {
136151
readonly #sessionId: string = crypto.randomUUID();
137152
#aidaClient: Host.AidaClient.AidaClient;
138153
#serverSideLoggingEnabled: boolean;
154+
#origin?: string;
139155
abstract readonly preamble: string;
140156
abstract readonly options: AidaRequestOptions;
141157
abstract readonly clientFeature: Host.AidaClient.ClientFeature;
142158
abstract readonly userTier: string|undefined;
143-
abstract handleContextDetails(select: T|null): AsyncGenerator<ContextResponse, void, void>;
159+
abstract handleContextDetails(select: ConversationContext<T>|null): AsyncGenerator<ContextResponse, void, void>;
144160

145161
/**
146162
* Mapping between the unique request id and
@@ -165,6 +181,10 @@ export abstract class AiAgent<T> {
165181
return this.#history.size <= 0;
166182
}
167183

184+
get origin(): string|undefined {
185+
return this.#origin;
186+
}
187+
168188
get title(): string|undefined {
169189
return [...this.#history.values()]
170190
.flat()
@@ -248,7 +268,7 @@ export abstract class AiAgent<T> {
248268
throw new Error('Unexpected action found');
249269
}
250270

251-
async enhanceQuery(query: string, selected: T|null): Promise<string>;
271+
async enhanceQuery(query: string, selected: ConversationContext<T>|null): Promise<string>;
252272
async enhanceQuery(query: string): Promise<string> {
253273
return query;
254274
}
@@ -359,8 +379,12 @@ STOP`;
359379

360380
#runId = 0;
361381
async * run(query: string, options: {
362-
signal?: AbortSignal, selected: T|null,
382+
signal?: AbortSignal, selected: ConversationContext<T>|null,
363383
}): AsyncGenerator<ResponseData, void, void> {
384+
// First context set on the agent determines its origin from now on.
385+
if (options.selected && this.#origin === undefined && options.selected) {
386+
this.#origin = options.selected.getOrigin();
387+
}
364388
const id = this.#runId++;
365389

366390
const response = {

front_end/panels/freestyler/DrJonesFileAgent.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {describeWithMockConnection} from '../../testing/MockConnection.js';
1616
import {loadBasicSourceMapExample} from '../../testing/SourceMapHelpers.js';
1717
import {createContentProviderUISourceCodes} from '../../testing/UISourceCodeHelpers.js';
1818

19-
import {DrJonesFileAgent, formatSourceMapDetails, ResponseType} from './freestyler.js';
19+
import {DrJonesFileAgent, FileContext, formatSourceMapDetails, ResponseType} from './freestyler.js';
2020

2121
describeWithMockConnection('DrJonesFileAgent', () => {
2222
function mockHostConfig(modelId?: string, temperature?: number) {
@@ -162,7 +162,8 @@ describeWithMockConnection('DrJonesFileAgent', () => {
162162
});
163163

164164
const uiSourceCode = project.uiSourceCodeForURL(url);
165-
const responses = await Array.fromAsync(agent.run('test', {selected: uiSourceCode}));
165+
const responses =
166+
await Array.fromAsync(agent.run('test', {selected: uiSourceCode ? new FileContext(uiSourceCode) : null}));
166167

167168
assert.deepStrictEqual(responses, [
168169
{

front_end/panels/freestyler/DrJonesFileAgent.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type AidaRequestOptions,
1515
type ContextDetail,
1616
type ContextResponse,
17+
ConversationContext,
1718
type ParsedResponse,
1819
ResponseType,
1920
} from './AiAgent.js';
@@ -69,6 +70,23 @@ const lockedString = i18n.i18n.lockedString;
6970

7071
const MAX_FILE_SIZE = 10000;
7172

73+
export class FileContext extends ConversationContext<Workspace.UISourceCode.UISourceCode> {
74+
#file: Workspace.UISourceCode.UISourceCode;
75+
76+
constructor(file: Workspace.UISourceCode.UISourceCode) {
77+
super();
78+
this.#file = file;
79+
}
80+
81+
getOrigin(): string {
82+
return new URL(this.#file.url()).origin;
83+
}
84+
85+
getItem(): Workspace.UISourceCode.UISourceCode {
86+
return this.#file;
87+
}
88+
}
89+
7290
/**
7391
* One agent instance handles one conversation. Create a new agent
7492
* instance for a new conversation.
@@ -93,7 +111,7 @@ export class DrJonesFileAgent extends AiAgent<Workspace.UISourceCode.UISourceCod
93111
}
94112

95113
async *
96-
handleContextDetails(selectedFile: Workspace.UISourceCode.UISourceCode|null):
114+
handleContextDetails(selectedFile: ConversationContext<Workspace.UISourceCode.UISourceCode>|null):
97115
AsyncGenerator<ContextResponse, void, void> {
98116
if (!selectedFile) {
99117
return;
@@ -106,9 +124,10 @@ export class DrJonesFileAgent extends AiAgent<Workspace.UISourceCode.UISourceCod
106124
};
107125
}
108126

109-
override async enhanceQuery(query: string, selectedFile: Workspace.UISourceCode.UISourceCode|null): Promise<string> {
127+
override async enhanceQuery(
128+
query: string, selectedFile: ConversationContext<Workspace.UISourceCode.UISourceCode>|null): Promise<string> {
110129
const fileEnchantmentQuery =
111-
selectedFile ? `# Selected file\n${formatFile(selectedFile)}\n\n# User request\n\n` : '';
130+
selectedFile ? `# Selected file\n${formatFile(selectedFile.getItem())}\n\n# User request\n\n` : '';
112131
return `${fileEnchantmentQuery}${query}`;
113132
}
114133

@@ -119,12 +138,12 @@ export class DrJonesFileAgent extends AiAgent<Workspace.UISourceCode.UISourceCod
119138
}
120139
}
121140

122-
function createContextDetailsForDrJonesFileAgent(selectedFile: Workspace.UISourceCode.UISourceCode):
123-
[ContextDetail, ...ContextDetail[]] {
141+
function createContextDetailsForDrJonesFileAgent(
142+
selectedFile: ConversationContext<Workspace.UISourceCode.UISourceCode>): [ContextDetail, ...ContextDetail[]] {
124143
return [
125144
{
126145
title: 'Selected file',
127-
text: formatFile(selectedFile),
146+
text: formatFile(selectedFile.getItem()),
128147
},
129148
];
130149
}

front_end/panels/freestyler/DrJonesNetworkAgent.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ import {describeWithMockConnection} from '../../testing/MockConnection.js';
1515
import {createNetworkPanelForMockConnection} from '../../testing/NetworkHelpers.js';
1616
import * as Coordinator from '../../ui/components/render_coordinator/render_coordinator.js';
1717

18-
import {allowHeader, DrJonesNetworkAgent, formatHeaders, formatInitiatorUrl, ResponseType} from './freestyler.js';
18+
import {
19+
allowHeader,
20+
DrJonesNetworkAgent,
21+
formatHeaders,
22+
formatInitiatorUrl,
23+
RequestContext,
24+
ResponseType,
25+
} from './freestyler.js';
1926

2027
const coordinator = Coordinator.RenderCoordinator.RenderCoordinator.instance();
2128

@@ -215,7 +222,8 @@ describeWithMockConnection('DrJonesNetworkAgent', () => {
215222
aidaClient: mockAidaClient(generateAnswer),
216223
});
217224

218-
const responses = await Array.fromAsync(agent.run('test', {selected: selectedNetworkRequest}));
225+
const responses =
226+
await Array.fromAsync(agent.run('test', {selected: new RequestContext(selectedNetworkRequest)}));
219227
assert.deepStrictEqual(responses, [
220228
{
221229
type: ResponseType.USER_QUERY,

front_end/panels/freestyler/DrJonesNetworkAgent.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
type AidaRequestOptions,
1616
type ContextDetail,
1717
type ContextResponse,
18+
ConversationContext,
1819
type ParsedResponse,
1920
ResponseType,
2021
} from './AiAgent.js';
@@ -97,6 +98,23 @@ const UIStringsNotTranslate = {
9798

9899
const lockedString = i18n.i18n.lockedString;
99100

101+
export class RequestContext extends ConversationContext<SDK.NetworkRequest.NetworkRequest> {
102+
#request: SDK.NetworkRequest.NetworkRequest;
103+
104+
constructor(request: SDK.NetworkRequest.NetworkRequest) {
105+
super();
106+
this.#request = request;
107+
}
108+
109+
getOrigin(): string {
110+
return new URL(this.#request.url()).origin;
111+
}
112+
113+
getItem(): SDK.NetworkRequest.NetworkRequest {
114+
return this.#request;
115+
}
116+
}
117+
100118
/**
101119
* One agent instance handles one conversation. Create a new agent
102120
* instance for a new conversation.
@@ -121,7 +139,7 @@ export class DrJonesNetworkAgent extends AiAgent<SDK.NetworkRequest.NetworkReque
121139
}
122140

123141
async *
124-
handleContextDetails(selectedNetworkRequest: SDK.NetworkRequest.NetworkRequest|null):
142+
handleContextDetails(selectedNetworkRequest: ConversationContext<SDK.NetworkRequest.NetworkRequest>|null):
125143
AsyncGenerator<ContextResponse, void, void> {
126144
if (!selectedNetworkRequest) {
127145
return;
@@ -130,14 +148,15 @@ export class DrJonesNetworkAgent extends AiAgent<SDK.NetworkRequest.NetworkReque
130148
yield {
131149
type: ResponseType.CONTEXT,
132150
title: lockedString(UIStringsNotTranslate.analyzingNetworkData),
133-
details: createContextDetailsForDrJonesNetworkAgent(selectedNetworkRequest),
151+
details: createContextDetailsForDrJonesNetworkAgent(selectedNetworkRequest.getItem()),
134152
};
135153
}
136154

137-
override async enhanceQuery(query: string, selectedNetworkRequest: SDK.NetworkRequest.NetworkRequest|null):
138-
Promise<string> {
155+
override async enhanceQuery(
156+
query: string,
157+
selectedNetworkRequest: ConversationContext<SDK.NetworkRequest.NetworkRequest>|null): Promise<string> {
139158
const networkEnchantmentQuery = selectedNetworkRequest ?
140-
`# Selected network request \n${formatNetworkRequest(selectedNetworkRequest)}\n\n# User request\n\n` :
159+
`# Selected network request \n${formatNetworkRequest(selectedNetworkRequest.getItem())}\n\n# User request\n\n` :
141160
'';
142161
return `${networkEnchantmentQuery}${query}`;
143162
}

front_end/panels/freestyler/DrJonesPerformanceAgent.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {describeWithEnvironment, getGetHostConfigStub} from '../../testing/Envir
77
import {TraceLoader} from '../../testing/TraceLoader.js';
88
import * as TimelineUtils from '../timeline/utils/utils.js';
99

10-
import {DrJonesPerformanceAgent, ResponseType} from './freestyler.js';
10+
import {CallTreeContext, DrJonesPerformanceAgent, ResponseType} from './freestyler.js';
1111

1212
describeWithEnvironment('DrJonesPerformanceAgent', () => {
1313
function mockHostConfig(modelId?: string, temperature?: number) {
@@ -133,7 +133,7 @@ describeWithEnvironment('DrJonesPerformanceAgent', () => {
133133
aidaClient: mockAidaClient(generateAnswer),
134134
});
135135

136-
const responses = await Array.fromAsync(agent.run('test', {selected: aiCallTree}));
136+
const responses = await Array.fromAsync(agent.run('test', {selected: new CallTreeContext(aiCallTree)}));
137137
const expectedData = '\n\n' +
138138
`
139139
@@ -198,7 +198,7 @@ self: 3
198198
serialize: () => 'Mock call tree',
199199
} as unknown as TimelineUtils.AICallTree.AICallTree;
200200

201-
const enhancedQuery1 = await agent.enhanceQuery('What is this?', mockAiCallTree);
201+
const enhancedQuery1 = await agent.enhanceQuery('What is this?', new CallTreeContext(mockAiCallTree));
202202
assert.strictEqual(enhancedQuery1, 'Mock call tree\n\n# User request\n\nWhat is this?');
203203

204204
// Create history state of the above query
@@ -227,13 +227,13 @@ self: 3
227227
]]);
228228

229229
const query2 = 'But what about this follow-up question?';
230-
const enhancedQuery2 = await agent.enhanceQuery(query2, mockAiCallTree);
230+
const enhancedQuery2 = await agent.enhanceQuery(query2, new CallTreeContext(mockAiCallTree));
231231
assert.strictEqual(enhancedQuery2, query2);
232232
assert.isFalse(enhancedQuery2.includes(mockAiCallTree.serialize()));
233233

234234
// Just making sure any subsequent chat doesnt include it either.
235235
const query3 = 'And this 3rd question?';
236-
const enhancedQuery3 = await agent.enhanceQuery(query3, mockAiCallTree);
236+
const enhancedQuery3 = await agent.enhanceQuery(query3, new CallTreeContext(mockAiCallTree));
237237
assert.strictEqual(enhancedQuery3, query3);
238238
assert.isFalse(enhancedQuery3.includes(mockAiCallTree.serialize()));
239239
});

front_end/panels/freestyler/DrJonesPerformanceAgent.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
AiAgent,
1313
type AidaRequestOptions,
1414
type ContextResponse,
15+
ConversationContext,
1516
type ParsedResponse,
1617
ResponseType,
1718
} from './AiAgent.js';
@@ -120,6 +121,24 @@ const UIStringsNotTranslate = {
120121

121122
const lockedString = i18n.i18n.lockedString;
122123

124+
export class CallTreeContext extends ConversationContext<TimelineUtils.AICallTree.AICallTree> {
125+
#callTree: TimelineUtils.AICallTree.AICallTree;
126+
127+
constructor(callTree: TimelineUtils.AICallTree.AICallTree) {
128+
super();
129+
this.#callTree = callTree;
130+
}
131+
132+
getOrigin(): string {
133+
// TODO: implement cross-origin checks for the PerformanceAgent.
134+
return '';
135+
}
136+
137+
getItem(): TimelineUtils.AICallTree.AICallTree {
138+
return this.#callTree;
139+
}
140+
}
141+
123142
/**
124143
* One agent instance handles one conversation. Create a new agent
125144
* instance for a new conversation.
@@ -144,22 +163,23 @@ export class DrJonesPerformanceAgent extends AiAgent<TimelineUtils.AICallTree.AI
144163
}
145164

146165
async *
147-
handleContextDetails(aiCallTree: TimelineUtils.AICallTree.AICallTree|null):
166+
handleContextDetails(aiCallTree: ConversationContext<TimelineUtils.AICallTree.AICallTree>|null):
148167
AsyncGenerator<ContextResponse, void, void> {
149168
yield {
150169
type: ResponseType.CONTEXT,
151170
title: lockedString(UIStringsNotTranslate.analyzingCallTree),
152171
details: [
153172
{
154173
title: 'Selected call tree',
155-
text: aiCallTree?.serialize() ?? '',
174+
text: aiCallTree?.getItem().serialize() ?? '',
156175
},
157176
],
158177
};
159178
}
160179

161-
override async enhanceQuery(query: string, aiCallTree: TimelineUtils.AICallTree.AICallTree|null): Promise<string> {
162-
const treeStr = aiCallTree?.serialize();
180+
override async enhanceQuery(query: string, aiCallTree: ConversationContext<TimelineUtils.AICallTree.AICallTree>|null):
181+
Promise<string> {
182+
const treeStr = aiCallTree?.getItem().serialize();
163183

164184
// Collect the queries from previous messages in this session
165185
const prevQueries: string[] = [];

0 commit comments

Comments
 (0)