Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6649d8a
fix(api): match pause points with jsonb_path_exists so a status-filte…
waleedlatif1 Nov 13, 2026
755dee5
fix(docs): copy section permalinks and polish outline navigation (#9972)
waleedlatif1 Nov 13, 2026
c02e06d
chore(analytics): remove Freebuff ad tracking and the Beluga short-li…
waleedlatif1 Nov 13, 2026
ce58845
fix(desktop): weak-link ScreenCaptureKit so the Computer Use helper l…
waleedlatif1 Nov 13, 2026
6c6e020
feat(demo): auto-confirm demo bookings that came through the verified…
waleedlatif1 Nov 13, 2026
d8d7c8c
improvement(credentials): resolve agent fallback credentials once, Va…
waleedlatif1 Nov 13, 2026
e09144a
improvement(computer-use): allow every app by default, protect sensit…
waleedlatif1 Nov 13, 2026
90f5b00
fix(docker): ship the realtime and migrations images with runtime dep…
waleedlatif1 Nov 13, 2026
cfc4083
improvement(executor): run the Agent block on the shared model-fallba…
waleedlatif1 Nov 13, 2026
5b4ce4a
improvement(search): shorter Jira any-term budget and a shared batche…
waleedlatif1 Nov 13, 2026
187e196
fix(aws-marketplace): check the container license with CheckoutLicens…
waleedlatif1 Nov 13, 2026
4180246
improvement(desktop-browser): let a page finish rendering before obse…
waleedlatif1 Nov 13, 2026
9c3aab4
fix(pii): mask texts over spaCy's length limit and drop encoded files…
waleedlatif1 Nov 13, 2026
66baa21
chore(upgrade): drop the stale Cal.com comment on the sales demo link…
waleedlatif1 Nov 13, 2026
3c4c4de
fix(search): check the last shortened read window against its budget …
waleedlatif1 Nov 13, 2026
f868409
chore(pii): drop the over-long text example from the mask client's ou…
waleedlatif1 Nov 13, 2026
2c6d89c
fix(computer-use): refuse Script Editor, Automator and Shortcuts, whi…
waleedlatif1 Nov 13, 2026
9bf50c9
fix(aws-marketplace): confirm a container license lapse on a second r…
waleedlatif1 Nov 13, 2026
990ea3a
fix(credentials): offer keyless Google delegation only for services t…
waleedlatif1 Nov 13, 2026
b97449a
fix(credentials): replay remembered refusals as credential errors, de…
waleedlatif1 Nov 13, 2026
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
6 changes: 6 additions & 0 deletions .github/actions/image/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ inputs:
description: Newline-separated Docker build arguments.
required: false
default: ''
target:
description: Dockerfile stage to build. Empty builds the final stage.
required: false
default: ''
max-cache-size-mb:
description: >-
Layer cache to retain after this action prunes, in MB. Must stay above one
Expand Down Expand Up @@ -77,6 +81,7 @@ runs:
push: true
tags: ${{ inputs.tags }}
build-args: ${{ inputs.build-args }}
target: ${{ inputs.target }}
provenance: false
sbom: false

Expand Down Expand Up @@ -183,5 +188,6 @@ runs:
push: true
tags: ${{ inputs.tags }}
build-args: ${{ inputs.build-args }}
target: ${{ inputs.target }}
provenance: false
sbom: false
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,11 @@ jobs:
ecr_repo_secret: ECR_APP
gh_runner: linux-x64-8-core
bs_runner: blacksmith-16vcpu-ubuntu-2404
# Dev runs `db:push`, which needs drizzle-kit; every other environment
# gets the final stage, which carries runtime dependencies only.
- id: db
dockerfile: ./docker/db.Dockerfile
target: dev
ecr_repo_secret: ECR_MIGRATIONS
gh_runner: ubuntu-latest
bs_runner: blacksmith-2vcpu-ubuntu-2404
Expand Down Expand Up @@ -259,6 +262,7 @@ jobs:
tags: ${{ steps.login-ecr.outputs.registry }}/${{ steps.ecr-repo.outputs.name }}:${{ github.sha }}-dev
build-args: |
MSHIP_PLAN_MODE_DEFAULT=true
target: ${{ matrix.target }}
max-cache-size-mb: ${{ matrix.cache_mb }}

# Staging/production coordinate task releases with app traffic cutover.
Expand Down
10 changes: 7 additions & 3 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,13 @@ Mothership's `computer` tool is available on macOS 14+ once the user opts in und
group with the **Computer Use** restriction. Older desktop builds omit this optional bridge and
continue to work.

The user grants Accessibility and Screen Recording through macOS, then approves each app for the
current task or persistently. Persistent app approvals are listed in Settings and can be revoked.
Sign-out and deployment changes clear computer-use approval and opt-in. The conversation shows the
The user grants Accessibility and Screen Recording through macOS; every app is then available except
Sim, System Settings, password managers, terminals and scripting apps (shell access has its own
Terminal control),
which the helper refuses. With **Ask before using a new
app** on, each app's first use is approved for the task or persistently, and persistent approvals are
listed in Settings and can be revoked. Sign-out and deployment changes clear opt-in, the ask choice
and approvals. The conversation shows the
active app/action and a Stop button; **⌘⇧Esc** also stops active work when the shortcut is available.
Stop cancels queued actions, in-flight authorization, approval prompts and the native helper.
Already completed input cannot be undone.
Expand Down
23 changes: 21 additions & 2 deletions apps/desktop/native/computer-use/ComputerUse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,29 @@ func elementRect(_ element: AXUIElement) -> CGRect? {
AXValueGetValue(unsafeBitCast(rawSize, to: AXValue.self), .cgSize, &size) else { return nil }
return CGRect(origin: position, size: size)
}
/// Apps computer use never operates, lowercased: macOS security settings, password managers, and apps
/// whose primary purpose is running shell commands or scripts (terminals, Script Editor, Automator,
/// Shortcuts), since shell access goes through the Terminal tool's own opt-in. Apps that merely embed a
/// terminal, such as editors, stay usable.
let PROTECTED_APPS: Set<String> = [
"com.apple.systempreferences", "com.apple.keychainaccess", "com.apple.passwords",
"com.1password.1password", "com.agilebits.onepassword7", "com.agilebits.onepassword-osx", "com.bitwarden.desktop",
"org.keepassxc.keepassxc", "com.lastpass.lastpass", "in.sinew.enpass-desktop", "me.proton.pass.electron",
"com.markmcguill.strongbox", "com.markmcguill.strongbox.mac",
"com.apple.terminal", "com.googlecode.iterm2", "dev.warp.warp-stable", "com.mitchellh.ghostty",
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
"net.kovidgoyal.kitty", "org.alacritty", "com.github.wez.wezterm", "co.zeit.hyper", "org.tabby",
"com.apple.scripteditor2", "com.apple.automator", "com.apple.shortcuts",
]
/// Whether computer use refuses an app: Sim's own apps and `PROTECTED_APPS`. Bundle identifiers match
/// case-insensitively in Launch Services.
func isProtectedApp(_ bundle: String) -> Bool {
let key = bundle.lowercased()
return key == Bundle.main.bundleIdentifier?.lowercased() || key.hasPrefix("com.simstudio.") || key.hasPrefix("ai.sim.desktop") || PROTECTED_APPS.contains(key)
}
func appFor(_ bundleId: String?, launch: Bool = false) async throws -> NSRunningApplication {
let bundle = try required(bundleId, "bundleId")
guard bundle != Bundle.main.bundleIdentifier, !bundle.hasPrefix("com.simstudio."), !bundle.hasPrefix("ai.sim.desktop"), bundle != "com.apple.systempreferences" else {
throw ComputerError("protected_app", "Computer use cannot control its own app or macOS security settings.")
guard !isProtectedApp(bundle) else {
throw ComputerError("protected_app", "Computer use does not operate Sim, System Settings, password managers, terminals, or scripting apps; use the terminal tool for shell work.")
}
var apps = NSRunningApplication.runningApplications(withBundleIdentifier: bundle).filter { !$0.isTerminated }
if apps.isEmpty && launch {
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/native/computer-use/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Persistent newline-delimited JSON over stdin/stdout: `{id,method,params}` return

The helper observes up to 2000 accessibility nodes (depth 64, queue 4000, traversal deadline 8 seconds, each AX call timeout 250ms). Selected-window and focused-element ancestry are prioritized. The menu bar and menus are read in place, so an open menu's items and their submenus come first; items of closed menus are read last, capped at 80 menu nodes. Electron accessibility is enabled using its documented `AXManualAccessibility` attribute. The helper observes 100 windows and 1000 apps. Requests are bounded to 128 KiB during ingestion. Text/value input is capped at 32000 UTF-16 units. State references are per-process, per-app, expire after 60 seconds, and are consumed by every action that reaches the app; a request that fails before sending anything reports `dispatchState: "not_started"` and keeps them valid. An Accessibility failure counts as sent unless Accessibility rejected the request before the app saw it (unsupported attribute or action, illegal argument). Restarts invalidate all references. Screen captures exclude window shadows, preserve the selected window’s coordinate geometry, and are capped to 1600 pixels on the longest side and 8 MiB encoded. Complete JSON replies are capped at 16 MiB: oversized observations omit screenshots first, then truncate accessibility nodes, while preserving action outcomes. Screenshots are optional and failure is reported in `screenshotError` without discarding accessibility state. macOS 26 uses the screenshot-specific capture API; the macOS 14/15 API path includes only the selected window in a display filter and crops its exact frame. That older path requires the window to fit within one display and reports `capture_geometry_unavailable` otherwise.

Coordinates in action inputs are window-local points; node frames are global screen points. Coordinate input without a `windowId` targets the window the snapshot selected, and an element target uses its own window; `press_key` with an `elementId` focuses that element first. Window bounds must still match the observed snapshot. App PID and launch time must match. Secure fields and descendants are unavailable as action targets; secure values and labels are suppressed. Sim's own apps and System Settings are protected targets. `get_app_state` can open an installed bundle ID without activating it. `list_apps` discovers installed apps under `/Applications`, `/System/Applications`, and `~/Applications` (bounded to 5000 filesystem entries and depth 3), merging running regular apps; installed apps omit `pid`.
Coordinates in action inputs are window-local points; node frames are global screen points. Coordinate input without a `windowId` targets the window the snapshot selected, and an element target uses its own window; `press_key` with an `elementId` focuses that element first. Window bounds must still match the observed snapshot. App PID and launch time must match. Secure fields and descendants are unavailable as action targets; secure values and labels are suppressed. Sim's own apps, System Settings, password managers, terminals and scripting apps (Script Editor, Automator, Shortcuts) are protected targets (`PROTECTED_APPS`, matched case-insensitively); shell work belongs to the separate Terminal control. `get_app_state` can open an installed bundle ID without activating it. `list_apps` discovers installed apps under `/Applications`, `/System/Applications`, and `~/Applications` (bounded to 5000 filesystem entries and depth 3), merging running regular apps; installed apps omit `pid`.

AXPress and AXSetValue are direct semantic operations. Keyboard events target the app PID. Cmd+A uses the focused editor's writable accessibility selection range where supported. Coordinate mouse input requires the target app in front; `activate_app` makes that focus change explicit and confirms it. Click/drag/scroll validate the foreground process, unchanged window geometry and the app that receives the point; the topmost window-list entry is not the click target because Notification Center and the Dock keep full-screen click-through windows. A hit on a window's own background counts as that window. Background tabs and minimized windows share bounds with the window in front, so they take no coordinate input. An action that opens UI (AXPress, AXPick, AXShowMenu) and times out while the app shows a sheet, modal or menu (`cannotComplete`) counts as dispatched; any other action that times out fails as uncertain. A menu-item press that leaves the item's own menu open with the app highlighting that item (Calendar on macOS 26) is completed with Return. Sheets, including Go to Folder and out-of-process open and save panels whose contents report no window, count as the window they are attached to. Menu item labels carry their shortcuts (for example ⇧⌘N). Some apps ignore background keyboard events. `dispatched:true,verified:false` means only dispatch succeeded; the agent must observe again to establish the outcome. No foreground activation fallback is hidden inside the helper. Native key codes currently assume the US layout for shortcuts; literal text uses Unicode events. Unsupported shortcuts fail explicitly.

Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/native/computer-use/tests/NativeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import AppKit
expectError("unsupported_key") { _ = try parseKey("F99") }
expectError("invalid_arguments") { _ = try parseKey("Hyper+A") }
expectError("invalid_arguments") { _ = try required(Optional<String>.none, "example") }
for bundle in ["com.apple.systempreferences", "com.apple.Terminal", "COM.GOOGLECODE.ITERM2", "net.kovidgoyal.kitty", "com.1password.1password", "com.apple.Passwords", "com.bitwarden.desktop", "ai.sim.desktop", "com.simstudio.computer-use", "com.apple.ScriptEditor2", "com.apple.Automator", "com.apple.shortcuts"] {
precondition(isProtectedApp(bundle), bundle)
}
for bundle in ["com.apple.TextEdit", "com.apple.finder", "com.apple.Safari"] { precondition(!isProtectedApp(bundle), bundle) }
var queue = AXTraversalQueue<String>(limit: 1000)
queue.append(AXTraversalEntry(element: "menu", parent: nil, depth: 0, menu: true, priority: false))
queue.append(AXTraversalEntry(element: "content", parent: nil, depth: 0, menu: false, priority: false))
Expand Down
24 changes: 24 additions & 0 deletions apps/desktop/scripts/build-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function compileComputerUse(): void {
try {
execFileSync('xcrun', ['lipo', binary, '-verify_arch', 'arm64', 'x86_64'])
execFileSync('codesign', ['--verify', '--strict', bundle])
assertWeakScreenCaptureKit(binary)
return
} catch {
rmSync(bundle, { recursive: true, force: true })
Expand All @@ -108,6 +109,11 @@ function compileComputerUse(): void {
'-O',
'-target',
`${arch}-apple-macosx14.0`,
// The macOS 26 screenshot classes do not exist on 14/15; a strong reference stops the helper loading there.
'-Xlinker',
'-weak_framework',
'-Xlinker',
'ScreenCaptureKit',
'native/computer-use/ComputerUse.swift',
'-o',
output,
Expand All @@ -118,9 +124,27 @@ function compileComputerUse(): void {
}
execFileSync('xcrun', ['lipo', '-create', ...parts, '-output', binary], { stdio: 'inherit' })
for (const part of parts) rmSync(part)
assertWeakScreenCaptureKit(binary)
execFileSync('codesign', ['--force', '--sign', '-', bundle], { stdio: 'inherit' })
recordNativeBuild('computer-use', outputs)
logger.info('Compiled native macOS Computer Use helper')
}

/**
* The helper targets macOS 14 but uses ScreenCaptureKit classes added later behind `#available`.
* Runners build on the newest macOS, where a strong reference still loads, so this is the only
* place a helper that dyld would refuse on older macOS is caught before it ships.
*/
function assertWeakScreenCaptureKit(binary: string) {
const strong = execFileSync('nm', ['-m', binary], { encoding: 'utf8' })
.split('\n')
.filter(
(line) => line.includes('(undefined) external') && line.includes('(from ScreenCaptureKit)')
)
if (strong.length > 0)
throw new Error(
`The Computer Use helper links ScreenCaptureKit strongly and would not load on macOS 14/15:\n${strong.join('\n')}`
)
}

if (args.includes('--computer-use')) compileComputerUse()
108 changes: 108 additions & 0 deletions apps/desktop/src/main/browser-agent/driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1894,6 +1894,7 @@ describe('credential protection', () => {
activeElementSecrecy: 'safe',
readActiveElementState: {},
readPageActionState: {},
readDomQuiet: { quietMs: 1_000, loading: false },
})
const pageCall = vi.mocked(contents.executeJavaScript).getMockImplementation()
let releaseObservation: (value: unknown) => void = () => {}
Expand Down Expand Up @@ -3212,6 +3213,113 @@ describe('credential protection', () => {
})
})

it.each([
['still rendering', [0, 40, 90, 200], 4],
['already quiet', [400], 1],
['never settling', Array.from({ length: 200 }, () => 0), undefined],
] as const)(
'observes after an action only once a page %s has gone quiet, within the settle cap',
async (_label, quietReadings, expectedProbes) => {
const contents = await openPage()
const calls: string[] = []
let probe = 0
vi.mocked(contents.executeJavaScript).mockImplementation((expression: string) => {
if (isPageCall(expression, 'readDomQuiet')) {
calls.push('probe')
const quietMs = quietReadings[Math.min(probe++, quietReadings.length - 1)]
return Promise.resolve({ quietMs, loading: false })
}
if (isPageCall(expression, 'collectSnapshot')) {
calls.push('snapshot')
return Promise.resolve({
url: 'https://example.com/',
title: 'Example',
outline: '- heading "Results"',
truncated: false,
refIds: [],
refLineIndexes: {},
nextElementId: 1_000,
})
}
if (isPageCall(expression, 'clickElement')) return Promise.resolve(CLICK_TARGET)
return Promise.resolve(undefined)
})
vi.useFakeTimers()
try {
const clicking = driver.executeTool('chat-test', 'browser_click', {
elementId: 0,
observe: {},
})
await vi.advanceTimersByTimeAsync(5_000)

expect(await clicking).toMatchObject({ ok: true, result: { observation: { ok: true } } })
const probesBeforeSnapshot = calls.indexOf('snapshot')
expect(probesBeforeSnapshot).toBeGreaterThan(0)
if (expectedProbes !== undefined) expect(probesBeforeSnapshot).toBe(expectedProbes)
} finally {
vi.useRealTimers()
}
}
)

it('keeps settling through failed quiet probes, and never observes once cancelled', async () => {
const contents = await openPage()
const calls: string[] = []
let failing = 2
vi.mocked(contents.executeJavaScript).mockImplementation((expression: string) => {
if (isPageCall(expression, 'readDomQuiet')) {
calls.push('probe')
if (failing > 0) {
failing--
vi.mocked(contents.isLoading).mockReturnValue(true)
return Promise.reject(new Error('Execution context was destroyed'))
}
vi.mocked(contents.isLoading).mockReturnValue(false)
return Promise.resolve({ quietMs: failing === -1 ? 0 : 400, loading: false })
}
if (isPageCall(expression, 'collectSnapshot')) {
calls.push('snapshot')
return Promise.resolve({
url: 'https://example.com/',
title: 'Example',
outline: '- heading "Results"',
truncated: false,
refIds: [],
refLineIndexes: {},
nextElementId: 1_000,
})
}
if (isPageCall(expression, 'clickElement')) return Promise.resolve(CLICK_TARGET)
return Promise.resolve(undefined)
})
vi.useFakeTimers()
try {
const settled = driver.executeTool('chat-test', 'browser_click', {
elementId: 0,
observe: {},
})
await vi.advanceTimersByTimeAsync(5_000)
expect(await settled).toMatchObject({ ok: true, result: { observation: { ok: true } } })
expect(calls.indexOf('snapshot')).toBe(3)

calls.length = 0
failing = -1
const cancelled = driver.executeTool(
'chat-test',
'browser_click',
{ elementId: 0, observe: { query: 'Results' } },
'click-cancelled'
)
await vi.advanceTimersByTimeAsync(300)
expect(driver.cancelTool('chat-test', 'click-cancelled')).toBe(true)
await vi.advanceTimersByTimeAsync(5_000)
await cancelled
expect(calls).not.toContain('snapshot')
} finally {
vi.useRealTimers()
}
})

it('acts on a ref an earlier snapshot of the same page issued', async () => {
const contents = await openPage()
respondWith(contents, {
Expand Down
Loading
Loading