Skip to content

Commit 126fbab

Browse files
committed
Merge commit '2e111f615c845a8e8a551e8e1f2b83846f89e334' into feat/organization-byok-inheritance
2 parents 219b2c6 + 2e111f6 commit 126fbab

476 files changed

Lines changed: 41713 additions & 6336 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test-build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ jobs:
123123
- name: Repo audits
124124
run: bun run check:audits
125125

126+
- name: Verify docs manifest is in sync
127+
run: bun run docs-manifest:check
128+
126129
- name: Migration safety (zero-downtime) audit
127130
run: |
128131
if [ "${{ github.event_name }}" = "pull_request" ]; then

apps/desktop/build/entitlements.mac.plist

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,15 @@
99
even after the user grants access in System Settings. -->
1010
<key>com.apple.security.device.audio-input</key>
1111
<true/>
12+
<!-- The agent browser joins real meetings (Google Meet, Zoom web): its
13+
getUserMedia grant is gated on the OS grant, and without this key the
14+
Hardened Runtime denies the camera no matter what the user allowed. -->
15+
<key>com.apple.security.device.camera</key>
16+
<true/>
17+
<!-- WebAuthn hybrid transport (passkey on the user's phone via QR) rides
18+
Bluetooth proximity; without this the QR option silently never
19+
completes in signed builds. -->
20+
<key>com.apple.security.device.bluetooth</key>
21+
<true/>
1222
</dict>
1323
</plist>

apps/desktop/electron-builder.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ mac:
5858
# macOS refuses to show the microphone prompt at all — it kills the process —
5959
# unless the bundle declares why it wants the device.
6060
extendInfo:
61-
NSMicrophoneUsageDescription: Sim uses your microphone for voice input in Chat.
61+
NSMicrophoneUsageDescription: Sim uses your microphone for voice input in Chat and for meetings you join in the built-in browser.
62+
NSCameraUsageDescription: Sim uses your camera for meetings you join in the built-in browser, such as Google Meet.
63+
NSBluetoothAlwaysUsageDescription: Sim uses Bluetooth to complete passkey sign-ins with a nearby phone in the built-in browser.
6264
entitlements: build/entitlements.mac.plist
6365
entitlementsInherit: build/entitlements.mac.plist
6466
notarize: true

apps/desktop/src/main/browser-agent/cdp.test.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { describe, expect, it, vi } from 'vitest'
22

33
vi.mock('electron', () => import('@/test/electron-mock'))
44

5-
import { WebContentsView, type WebFrameMain } from 'electron'
5+
import { nativeImage, type WebContents, WebContentsView, type WebFrameMain } from 'electron'
66
import {
7+
captureScreenshot,
78
clickAt,
89
ensureInstrumented,
910
evaluateInIsolatedFrame,
@@ -482,3 +483,89 @@ describe('browser-agent CDP theme', () => {
482483
})
483484
})
484485
})
486+
487+
/**
488+
* The browser panel shows a LIVE view, so a capture must not perturb the page.
489+
* Chromium serves `clip` by applying device-emulation params to the widget and
490+
* syncing visual properties, which the user sees as the page rescaling and
491+
* snapping back. Resolution is bounded on the returned image instead.
492+
*/
493+
describe('browser-agent screenshot capture', () => {
494+
function captureFixture(imageSize: { width: number; height: number } | null) {
495+
const contents = new WebContentsView().webContents
496+
vi.mocked(contents.debugger.sendCommand).mockImplementation((method: string) => {
497+
if (method === 'Page.getLayoutMetrics') {
498+
return Promise.resolve({ cssLayoutViewport: { clientWidth: 2048, clientHeight: 1024 } })
499+
}
500+
if (method === 'Page.captureScreenshot') return Promise.resolve({ data: 'c2lt' })
501+
return Promise.resolve(undefined)
502+
})
503+
const resized = {
504+
toJPEG: vi.fn(() => Buffer.from('resized')),
505+
}
506+
// Shared module-level mock: without this, a later fixture reads the
507+
// earlier test's decoded image.
508+
vi.mocked(nativeImage.createFromBuffer).mockReset()
509+
vi.mocked(nativeImage.createFromBuffer).mockReturnValue({
510+
isEmpty: vi.fn(() => imageSize === null),
511+
getSize: vi.fn(() => imageSize ?? { width: 0, height: 0 }),
512+
resize: vi.fn(() => resized),
513+
toJPEG: vi.fn(() => Buffer.alloc(0)),
514+
} as unknown as ReturnType<typeof nativeImage.createFromBuffer>)
515+
return { contents, resized }
516+
}
517+
518+
function screenshotParams(contents: WebContents): Record<string, unknown> {
519+
const call = vi
520+
.mocked(contents.debugger.sendCommand)
521+
.mock.calls.find(([method]) => method === 'Page.captureScreenshot')
522+
if (!call) throw new Error('no capture was requested')
523+
return call[1] as Record<string, unknown>
524+
}
525+
526+
it('never sends a clip, which would emulate the live page for the capture', async () => {
527+
const { contents } = captureFixture({ width: 4096, height: 2048 })
528+
529+
await captureScreenshot(contents)
530+
531+
expect(screenshotParams(contents)).not.toHaveProperty('clip')
532+
})
533+
534+
/**
535+
* A 2048px CSS viewport bounded to 1024px is scale 0.5, and the capture
536+
* arrives at device resolution (4096px on a 2x display). The resize is what
537+
* lands the image on the CSS-relative size the coordinate contract
538+
* (cssX = imageX / scale) assumes.
539+
*/
540+
it('downscales the returned image to the CSS-relative size', async () => {
541+
const { contents, resized } = captureFixture({ width: 4096, height: 2048 })
542+
543+
const shot = await captureScreenshot(contents)
544+
545+
const image = vi.mocked(nativeImage.createFromBuffer).mock.results[0].value
546+
expect(image.resize).toHaveBeenCalledWith({ width: 1024, height: 512, quality: 'good' })
547+
expect(resized.toJPEG).toHaveBeenCalled()
548+
expect(shot).toEqual({
549+
dataUrl: `data:image/jpeg;base64,${Buffer.from('resized').toString('base64')}`,
550+
scale: 0.5,
551+
})
552+
})
553+
554+
it('skips the re-encode when the capture already matches the target size', async () => {
555+
const { contents } = captureFixture({ width: 1024, height: 512 })
556+
557+
const shot = await captureScreenshot(contents)
558+
559+
const image = vi.mocked(nativeImage.createFromBuffer).mock.results[0].value
560+
expect(image.resize).not.toHaveBeenCalled()
561+
expect(shot).toEqual({ dataUrl: 'data:image/jpeg;base64,c2lt', scale: 0.5 })
562+
})
563+
564+
it('returns the raw capture when the image cannot be decoded', async () => {
565+
const { contents } = captureFixture(null)
566+
567+
const shot = await captureScreenshot(contents)
568+
569+
expect(shot).toEqual({ dataUrl: 'data:image/jpeg;base64,c2lt', scale: 0.5 })
570+
})
571+
})

0 commit comments

Comments
 (0)