Skip to content

Commit 9d2dba8

Browse files
author
SentienceDEV
committed
solution for handle Chrome permission popup
1 parent 0237f48 commit 9d2dba8

File tree

7 files changed

+220
-4
lines changed

7 files changed

+220
-4
lines changed

src/agent-runtime.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,17 @@ export class AgentRuntime {
492492
const hasEval = typeof (this as any).evaluateJs === 'function';
493493
const hasKeyboard = Boolean((this.page as any)?.keyboard);
494494
const hasDownloads = this.downloads.length >= 0;
495+
let hasPermissions = false;
496+
try {
497+
const context =
498+
typeof (this.page as any)?.context === 'function' ? (this.page as any).context() : null;
499+
hasPermissions =
500+
Boolean(context) &&
501+
typeof context.clearPermissions === 'function' &&
502+
typeof context.grantPermissions === 'function';
503+
} catch {
504+
hasPermissions = false;
505+
}
495506
let hasFiles = false;
496507
if (this.toolRegistry) {
497508
hasFiles = Boolean(this.toolRegistry.get('read_file'));
@@ -502,6 +513,7 @@ export class AgentRuntime {
502513
downloads: hasDownloads,
503514
filesystem_tools: hasFiles,
504515
keyboard: hasKeyboard,
516+
permissions: hasPermissions,
505517
};
506518
}
507519

src/browser.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ import { SnapshotOptions } from './snapshot';
1212
import { IBrowser } from './protocols/browser-protocol';
1313
import { snapshot as snapshotFunction } from './snapshot';
1414

15+
export type PermissionDefault = 'clear' | 'deny' | 'grant';
16+
17+
export type PermissionPolicy = {
18+
default?: PermissionDefault;
19+
autoGrant?: string[];
20+
geolocation?: { latitude: number; longitude: number; accuracy?: number };
21+
origin?: string;
22+
};
23+
1524
export function normalizeDomain(domain: string): string {
1625
const raw = domain.trim();
1726
let host = raw;
@@ -88,6 +97,7 @@ export class SentienceBrowser implements IBrowser {
8897
private _allowedDomains?: string[];
8998
private _prohibitedDomains?: string[];
9099
private _keepAlive: boolean;
100+
private _permissionPolicy?: PermissionPolicy;
91101

92102
/**
93103
* Create a new SentienceBrowser instance
@@ -121,7 +131,8 @@ export class SentienceBrowser implements IBrowser {
121131
deviceScaleFactor?: number,
122132
allowedDomains?: string[],
123133
prohibitedDomains?: string[],
124-
keepAlive: boolean = false
134+
keepAlive: boolean = false,
135+
permissionPolicy?: PermissionPolicy
125136
) {
126137
this._apiKey = apiKey;
127138

@@ -162,6 +173,23 @@ export class SentienceBrowser implements IBrowser {
162173
this._allowedDomains = allowedDomains;
163174
this._prohibitedDomains = prohibitedDomains;
164175
this._keepAlive = keepAlive;
176+
this._permissionPolicy = permissionPolicy;
177+
}
178+
179+
private async applyPermissionPolicy(
180+
context: BrowserContext,
181+
policy: PermissionPolicy
182+
): Promise<void> {
183+
const defaultPolicy = policy.default ?? 'clear';
184+
if (defaultPolicy === 'clear' || defaultPolicy === 'deny') {
185+
await context.clearPermissions();
186+
}
187+
if (policy.geolocation) {
188+
await context.setGeolocation(policy.geolocation);
189+
}
190+
if (policy.autoGrant && policy.autoGrant.length > 0) {
191+
await context.grantPermissions(policy.autoGrant, policy.origin);
192+
}
165193
}
166194

167195
async start(): Promise<void> {
@@ -276,6 +304,10 @@ export class SentienceBrowser implements IBrowser {
276304

277305
this.context = await chromium.launchPersistentContext(this.userDataDir, launchOptions);
278306

307+
if (this._permissionPolicy) {
308+
await this.applyPermissionPolicy(this.context, this._permissionPolicy);
309+
}
310+
279311
this.page = this.context.pages()[0] || (await this.context.newPage());
280312

281313
// Inject storage state if provided (must be after context creation)

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Sentience TypeScript SDK - AI Agent Browser Automation
33
*/
44

5-
export { SentienceBrowser } from './browser';
5+
export { SentienceBrowser, PermissionPolicy } from './browser';
66
export { snapshot, SnapshotOptions } from './snapshot';
77
export { query, find, parseSelector } from './query';
88
export {

src/tools/defaults.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from 'zod';
22
import type { AgentRuntime } from '../agent-runtime';
33
import type { ActionResult, Snapshot, EvaluateJsResult } from '../types';
4-
import { ToolContext } from './context';
4+
import { ToolContext, UnsupportedCapabilityError } from './context';
55
import { defineTool, ToolRegistry } from './registry';
66

77
const snapshotSchema = z
@@ -83,6 +83,19 @@ const evaluateJsInput = z.object({
8383
truncate: z.boolean().default(true),
8484
});
8585

86+
const grantPermissionsInput = z.object({
87+
permissions: z.array(z.string()).min(1),
88+
origin: z.string().optional(),
89+
});
90+
91+
const clearPermissionsInput = z.object({});
92+
93+
const setGeolocationInput = z.object({
94+
latitude: z.number(),
95+
longitude: z.number(),
96+
accuracy: z.number().optional(),
97+
});
98+
8699
function getRuntime(ctx: ToolContext | null, runtime?: ToolContext | AgentRuntime): AgentRuntime {
87100
if (ctx) return ctx.runtime;
88101
if (runtime instanceof ToolContext) return runtime.runtime;
@@ -342,5 +355,87 @@ export function registerDefaultTools(
342355
})
343356
);
344357

358+
registry.register(
359+
defineTool<z.infer<typeof grantPermissionsInput>, ActionResult, ToolContext | null>({
360+
name: 'grant_permissions',
361+
description: 'Grant browser permissions for the current context.',
362+
input: grantPermissionsInput,
363+
output: actionResultSchema,
364+
handler: async (ctx, params): Promise<ActionResult> => {
365+
const runtimeRef = getRuntime(ctx, runtime);
366+
if (ctx) {
367+
ctx.require('permissions');
368+
} else if (!runtimeRef.can('permissions')) {
369+
throw new UnsupportedCapabilityError('permissions');
370+
}
371+
const context =
372+
typeof (runtimeRef.page as any)?.context === 'function'
373+
? (runtimeRef.page as any).context()
374+
: null;
375+
if (!context) {
376+
throw new Error('Permission context unavailable');
377+
}
378+
await context.grantPermissions(params.permissions, params.origin);
379+
return { success: true, duration_ms: 0, outcome: 'dom_updated' };
380+
},
381+
})
382+
);
383+
384+
registry.register(
385+
defineTool<z.infer<typeof clearPermissionsInput>, ActionResult, ToolContext | null>({
386+
name: 'clear_permissions',
387+
description: 'Clear browser permissions for the current context.',
388+
input: clearPermissionsInput,
389+
output: actionResultSchema,
390+
handler: async (ctx): Promise<ActionResult> => {
391+
const runtimeRef = getRuntime(ctx, runtime);
392+
if (ctx) {
393+
ctx.require('permissions');
394+
} else if (!runtimeRef.can('permissions')) {
395+
throw new UnsupportedCapabilityError('permissions');
396+
}
397+
const context =
398+
typeof (runtimeRef.page as any)?.context === 'function'
399+
? (runtimeRef.page as any).context()
400+
: null;
401+
if (!context) {
402+
throw new Error('Permission context unavailable');
403+
}
404+
await context.clearPermissions();
405+
return { success: true, duration_ms: 0, outcome: 'dom_updated' };
406+
},
407+
})
408+
);
409+
410+
registry.register(
411+
defineTool<z.infer<typeof setGeolocationInput>, ActionResult, ToolContext | null>({
412+
name: 'set_geolocation',
413+
description: 'Set geolocation for the current browser context.',
414+
input: setGeolocationInput,
415+
output: actionResultSchema,
416+
handler: async (ctx, params): Promise<ActionResult> => {
417+
const runtimeRef = getRuntime(ctx, runtime);
418+
if (ctx) {
419+
ctx.require('permissions');
420+
} else if (!runtimeRef.can('permissions')) {
421+
throw new UnsupportedCapabilityError('permissions');
422+
}
423+
const context =
424+
typeof (runtimeRef.page as any)?.context === 'function'
425+
? (runtimeRef.page as any).context()
426+
: null;
427+
if (!context) {
428+
throw new Error('Permission context unavailable');
429+
}
430+
await context.setGeolocation({
431+
latitude: params.latitude,
432+
longitude: params.longitude,
433+
accuracy: params.accuracy,
434+
});
435+
return { success: true, duration_ms: 0, outcome: 'dom_updated' };
436+
},
437+
})
438+
);
439+
345440
return registry;
346441
}

src/tools/registry.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@ export class ToolRegistry {
8484
name: string,
8585
payload: unknown,
8686
ctx: {
87-
runtime?: { tracer?: { emit: (...args: any[]) => void }; stepId?: string; step_id?: string };
87+
runtime?: {
88+
tracer?: { emit: (...args: any[]) => void };
89+
stepId?: string | null;
90+
step_id?: string | null;
91+
};
8892
} | null = null
8993
): Promise<TOutput> {
9094
const start = Date.now();

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ export interface BackendCapabilities {
259259
downloads: boolean;
260260
filesystem_tools: boolean;
261261
keyboard: boolean;
262+
permissions: boolean;
262263
}
263264

264265
export interface EvaluateJsRequest {

tests/tool-registry.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import path from 'path';
44
import { z } from 'zod';
55
import { defineTool, ToolRegistry } from '../src/tools/registry';
66
import { FileSandbox, registerFilesystemTools } from '../src/tools/filesystem';
7+
import { registerDefaultTools } from '../src/tools/defaults';
8+
import { ToolContext, UnsupportedCapabilityError } from '../src/tools/context';
79

810
describe('ToolRegistry', () => {
911
it('validates and executes tools', async () => {
@@ -35,3 +37,73 @@ describe('Filesystem tools', () => {
3537
expect(result.content).toBe('hi');
3638
});
3739
});
40+
41+
describe('Permission tools', () => {
42+
it('grants permissions when supported', async () => {
43+
const registry = new ToolRegistry();
44+
const calls: Array<Record<string, any>> = [];
45+
const contextStub = {
46+
grantPermissions: (permissions: string[], origin?: string) => {
47+
calls.push({ kind: 'grant', permissions, origin });
48+
return Promise.resolve();
49+
},
50+
clearPermissions: () => Promise.resolve(),
51+
setGeolocation: () => Promise.resolve(),
52+
};
53+
54+
class RuntimeStub {
55+
page = { context: () => contextStub };
56+
capabilities() {
57+
return {
58+
tabs: false,
59+
evaluate_js: false,
60+
downloads: false,
61+
filesystem_tools: false,
62+
keyboard: false,
63+
permissions: true,
64+
};
65+
}
66+
can(name: keyof ReturnType<RuntimeStub['capabilities']>) {
67+
return Boolean(this.capabilities()[name]);
68+
}
69+
}
70+
71+
const ctx = new ToolContext(new RuntimeStub() as any);
72+
registerDefaultTools(registry, ctx);
73+
await registry.execute(
74+
'grant_permissions',
75+
{ permissions: ['geolocation'], origin: 'https://x.com' },
76+
ctx
77+
);
78+
expect(calls).toEqual([
79+
{ kind: 'grant', permissions: ['geolocation'], origin: 'https://x.com' },
80+
]);
81+
});
82+
83+
it('rejects permissions when unsupported', async () => {
84+
const registry = new ToolRegistry();
85+
86+
class RuntimeStub {
87+
page = { context: () => null };
88+
capabilities() {
89+
return {
90+
tabs: false,
91+
evaluate_js: false,
92+
downloads: false,
93+
filesystem_tools: false,
94+
keyboard: false,
95+
permissions: false,
96+
};
97+
}
98+
can(name: keyof ReturnType<RuntimeStub['capabilities']>) {
99+
return Boolean(this.capabilities()[name]);
100+
}
101+
}
102+
103+
const ctx = new ToolContext(new RuntimeStub() as any);
104+
registerDefaultTools(registry, ctx);
105+
await expect(
106+
registry.execute('grant_permissions', { permissions: ['geolocation'] }, ctx)
107+
).rejects.toBeInstanceOf(UnsupportedCapabilityError);
108+
});
109+
});

0 commit comments

Comments
 (0)