Skip to content

Commit 833c3af

Browse files
authored
v0.9.55: saved credentials for databases, Bedrock, and more services with configurable keyless audiences, computer use on by default behind a permission-group restriction, AWS Marketplace listing catalog and usage billing, Google, Microsoft, Atlassian, and AWS integration validation, GitLab permission export
2 parents 2124b78 + 85b731a commit 833c3af

1,335 files changed

Lines changed: 76464 additions & 6072 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/checks.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ jobs:
149149
working-directory: packages/db
150150
run: |
151151
bun -e 'import postgres from "postgres"; const sql = postgres(process.env.DATABASE_URL); for (const extension of ["vector", "btree_gin", "pg_trgm"]) await sql`CREATE EXTENSION IF NOT EXISTS ${sql(extension)}`; await sql.end()'
152+
# A test that hangs on a lock leaves only its timeout behind; these name the waiter, the
153+
# lock, and the blocking statement in the Postgres log. Service containers take no -c flags,
154+
# and ALTER SYSTEM runs outside a transaction, so each setting is its own statement.
155+
bun -e 'import postgres from "postgres"; const sql = postgres(process.env.DATABASE_URL); for (const setting of ["log_lock_waits = on", "deadlock_timeout = 1000", "log_min_duration_statement = 20000"]) await sql.unsafe(`ALTER SYSTEM SET ${setting}`); await sql`SELECT pg_reload_conf()`; await sql.end()'
152156
bun run db:${{ matrix.provision }}
153157
154158
- name: Verify migration replay is a no-op

‎.github/workflows/docs-negotiation.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ jobs:
268268
DOCS_GENERATORS_REPORT_PATH: ${{ github.workspace }}/apps/docs/test-results/generators/report.json
269269
DOCS_COMPATIBILITY_REPORT_PATH: ${{ github.workspace }}/apps/docs/test-results/compatibility.json
270270
DOCS_HEADER_REPORT_PATH: ${{ github.workspace }}/apps/docs/test-results/docs-headers.json
271+
GITLAB_EXPORT_REPORT_PATH: ${{ github.workspace }}/apps/docs/test-results/gitlab-export.json
271272
steps:
272273
- name: Checkout code
273274
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
@@ -292,6 +293,8 @@ jobs:
292293
- name: Verify native header edge cases
293294
if: ${{ !cancelled() && steps.generators.outcome != 'skipped' }}
294295
run: bun run --cwd apps/docs test:headers:e2e
296+
- name: Verify GitLab CSV export over HTTPS
297+
run: python3 apps/docs/scripts/test-gitlab-export-e2e.py
295298
- name: Bound the generated content cache
296299
if: always()
297300
run: bash .github/actions/docs-content-cache/prune.sh content

‎apps/desktop/README.md‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -234,11 +234,10 @@ Cadence: Electron ships a major every ~8 weeks and supports the latest 3 — bud
234234

235235
## Native Mac computer use
236236

237-
Mothership's `computer` tool is available on macOS 14+ when the global runtime flag
238-
`mothership-computer-use` is enabled and the user opts in under **Settings → Desktop → Computer Use**.
239-
The flag defaults off; local/self-hosted deployments use `MSHIP_COMPUTER_USE=true`, while hosted
240-
rollout uses the existing AppConfig feature-flags document. Older desktop builds omit this optional
241-
bridge and continue to work.
237+
Mothership's `computer` tool is available on macOS 14+ once the user opts in under
238+
**Settings → Desktop → Computer Use**. Enterprise organizations can withhold it from a permission
239+
group with the **Computer Use** restriction. Older desktop builds omit this optional bridge and
240+
continue to work.
242241

243242
The user grants Accessibility and Screen Recording through macOS, then approves each app for the
244243
current task or persistently. Persistent app approvals are listed in Settings and can be revoked.

‎apps/desktop/e2e/browser-tools.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1365,7 +1365,7 @@ test.describe('browser tools', () => {
13651365
expect(await formState()).toMatchObject({ name: 'Observed value' })
13661366
const result = fill.result as { observation: { result: { matches: { elementId: number }[] } } }
13671367
const freshId = result.observation.result.matches[0].elementId
1368-
expect(freshId).not.toBe(ref('Name'))
1368+
expect(freshId).toBe(ref('Name'))
13691369
const clear = await execute('browser_type', { elementId: freshId, text: '', observe: {} })
13701370
expect(clear.ok, clear.error).toBe(true)
13711371
expect(clear.result).toMatchObject({ observation: { ok: true } })

‎apps/desktop/e2e/local-files.spec.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ test('native file tools remember folder consent across chats and restarts until
6060
stdin: {
6161
contents: `import { createRoot } from 'react-dom/client';
6262
import { ToastProvider } from '@sim/emcn';
63+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6364
import { AppRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime';
6465
import { PathParamsContext } from 'next/dist/shared/lib/hooks-client-context.shared-runtime';
6566
import { Desktop } from '@/app/workspace/[workspaceId]/settings/components/desktop/desktop';
@@ -70,11 +71,11 @@ const router = { bfcacheId: 'fixture', back: () => history.back(), forward: () =
7071
createRoot(document.getElementById('settings')).render(
7172
<AppRouterContext.Provider value={router}>
7273
<PathParamsContext.Provider value={{ workspaceId: 'fixture' }}>
73-
<ToastProvider><SettingsHeaderProvider><SettingsHeaderShell>
74+
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}><ToastProvider><SettingsHeaderProvider><SettingsHeaderShell>
7475
<FeatureFlagsProvider flags={{ dashboards: false, 'workflow-tests': false, 'mothership-model-selector': false, 'mothership-plan-mode': false }}>
7576
<SettingsSectionProvider plane='workspace' section='desktop'><Desktop /></SettingsSectionProvider>
7677
</FeatureFlagsProvider>
77-
</SettingsHeaderShell></SettingsHeaderProvider></ToastProvider>
78+
</SettingsHeaderShell></SettingsHeaderProvider></ToastProvider></QueryClientProvider>
7879
</PathParamsContext.Provider>
7980
</AppRouterContext.Provider>
8081
);`,

‎apps/desktop/src/main/browser-agent/driver.test.ts‎

Lines changed: 178 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,13 +1507,13 @@ describe('browserToolWatchdogMs', () => {
15071507
})
15081508

15091509
it.each([
1510-
['number', 30_000, 35_000],
1511-
['numeric string', '30000', 35_000],
1512-
['absent', undefined, 15_000],
1513-
['non-numeric', 'soon', 15_000],
1514-
['zero', 0, 15_000],
1515-
['negative', -5_000, 15_000],
1516-
['above the wait clamp', 500_000, 125_000],
1510+
['number', 30_000, 55_000],
1511+
['numeric string', '30000', 55_000],
1512+
['absent', undefined, 35_000],
1513+
['non-numeric', 'soon', 35_000],
1514+
['zero', 0, 35_000],
1515+
['negative', -5_000, 35_000],
1516+
['above the wait clamp', 500_000, 145_000],
15171517
])('normalizes browser_wait_for timeout (%s)', (_label, timeoutMs, expected) => {
15181518
const params = timeoutMs === undefined ? {} : { timeoutMs }
15191519

@@ -3098,40 +3098,180 @@ describe('credential protection', () => {
30983098
})
30993099
})
31003100

3101+
it('gives a wait that runs to its timeout a full snapshot budget for its closing observation', async () => {
3102+
const contents = await openPage()
3103+
let finishSnapshot: (value: unknown) => void = () => {}
3104+
vi.mocked(contents.executeJavaScript).mockImplementation((expression: string) => {
3105+
if (!isPageCall(expression, 'collectSnapshot')) return Promise.resolve(undefined)
3106+
return new Promise((resolve) => {
3107+
finishSnapshot = resolve
3108+
})
3109+
})
3110+
vi.useFakeTimers()
3111+
try {
3112+
const waiting = driver.executeTool('chat-test', 'browser_wait_for', { timeoutMs: 1_000 })
3113+
await vi.advanceTimersByTimeAsync(9_000)
3114+
finishSnapshot({
3115+
url: 'https://example.com/',
3116+
title: 'Example',
3117+
outline: '- heading "Order confirmed"',
3118+
truncated: false,
3119+
refIds: [],
3120+
refLineIndexes: {},
3121+
nextElementId: 1_000,
3122+
})
3123+
await vi.advanceTimersByTimeAsync(1_000)
3124+
3125+
expect(await waiting).toMatchObject({
3126+
ok: true,
3127+
result: { observation: { ok: true, result: { outline: '- heading "Order confirmed"' } } },
3128+
})
3129+
} finally {
3130+
vi.useRealTimers()
3131+
}
3132+
})
3133+
3134+
it('accepts a later snapshot that keeps refs this scope issued for the same page', async () => {
3135+
const contents = await openPage()
3136+
const snapshot = (refIds: number[], nextElementId: number) => ({
3137+
url: 'https://example.com/',
3138+
title: 'Example',
3139+
outline: refIds.map((ref) => `- button "B${ref}" [ref=${ref}]`).join('\n'),
3140+
truncated: false,
3141+
refIds,
3142+
refLineIndexes: Object.fromEntries(refIds.map((ref, index) => [ref, index])),
3143+
nextElementId,
3144+
})
3145+
respondWith(contents, { collectSnapshot: snapshot([4, 5], 6) })
3146+
await driver.executeTool('chat-test', 'browser_snapshot', {})
3147+
respondWith(contents, { collectSnapshot: snapshot([4, 6], 7) })
3148+
3149+
const kept = await driver.executeTool('chat-test', 'browser_snapshot', {})
3150+
expect(kept).toMatchObject({ ok: true })
3151+
respondWith(contents, { collectSnapshot: snapshot([2], 7) })
3152+
3153+
const forged = await driver.executeTool('chat-test', 'browser_snapshot', {})
3154+
expect(forged).toMatchObject({ ok: false })
3155+
})
3156+
3157+
it("forgets a page's oldest refs once its history passes the retention bound", async () => {
3158+
const contents = await openPage()
3159+
const snapshot = (refIds: number[], nextElementId: number) => ({
3160+
url: 'https://example.com/',
3161+
title: 'Example',
3162+
outline: refIds.map((ref) => `- button "B${ref}" [ref=${ref}]`).join('\n'),
3163+
truncated: false,
3164+
refIds,
3165+
refLineIndexes: Object.fromEntries(refIds.map((ref, index) => [ref, index])),
3166+
nextElementId,
3167+
})
3168+
let next = 0
3169+
for (let round = 0; round < 19; round++) {
3170+
const refIds = Array.from({ length: 300 }, (_, index) => next + index)
3171+
next += 300
3172+
respondWith(contents, { collectSnapshot: snapshot(refIds, next) })
3173+
expect(await driver.executeTool('chat-test', 'browser_snapshot', {})).toMatchObject({
3174+
ok: true,
3175+
})
3176+
}
3177+
respondWith(contents, { collectSnapshot: snapshot([next - 1], next) })
3178+
expect(await driver.executeTool('chat-test', 'browser_snapshot', {})).toMatchObject({
3179+
ok: true,
3180+
})
3181+
respondWith(contents, { collectSnapshot: snapshot([0], next) })
3182+
3183+
expect(await driver.executeTool('chat-test', 'browser_snapshot', {})).toMatchObject({
3184+
ok: false,
3185+
})
3186+
})
3187+
3188+
it('recaptures with fresh ids when the page reuses an id this scope never committed', async () => {
3189+
const contents = await openPage()
3190+
const snapshot = (refIds: number[], nextElementId: number) => ({
3191+
url: 'https://example.com/',
3192+
title: 'Example',
3193+
outline: refIds.map((ref) => `- button "B${ref}" [ref=${ref}]`).join('\n'),
3194+
truncated: false,
3195+
refIds,
3196+
refLineIndexes: Object.fromEntries(refIds.map((ref, index) => [ref, index])),
3197+
nextElementId,
3198+
})
3199+
respondWith(contents, { collectSnapshot: snapshot([4, 5], 6) })
3200+
await driver.executeTool('chat-test', 'browser_snapshot', {})
3201+
vi.mocked(contents.executeJavaScript).mockImplementation((expression: string) => {
3202+
if (!isPageCall(expression, 'collectSnapshot')) return Promise.resolve(undefined)
3203+
const forgets = expression.includes(',true])')
3204+
return Promise.resolve(forgets ? snapshot([6, 7], 8) : snapshot([2, 6], 7))
3205+
})
3206+
3207+
const result = await driver.executeTool('chat-test', 'browser_snapshot', {})
3208+
3209+
expect(result).toMatchObject({
3210+
ok: true,
3211+
result: { outline: expect.stringContaining('[ref=7]') },
3212+
})
3213+
})
3214+
3215+
it('acts on a ref an earlier snapshot of the same page issued', async () => {
3216+
const contents = await openPage()
3217+
respondWith(contents, {
3218+
collectSnapshot: {
3219+
url: 'https://example.com/',
3220+
title: 'Example',
3221+
outline: '- button "Pins" [ref=4]\n- button "Files" [ref=5]',
3222+
truncated: false,
3223+
refIds: [4, 5],
3224+
refLineIndexes: { 4: 0, 5: 1 },
3225+
nextElementId: 6,
3226+
},
3227+
})
3228+
await driver.executeTool('chat-test', 'browser_snapshot', {})
3229+
respondWith(contents, {
3230+
collectSnapshot: {
3231+
url: 'https://example.com/',
3232+
title: 'Example',
3233+
outline: '- dialog "Pick a time" [ref=6]',
3234+
truncated: false,
3235+
refIds: [6],
3236+
refLineIndexes: { 6: 0 },
3237+
nextElementId: 7,
3238+
},
3239+
})
3240+
await driver.executeTool('chat-test', 'browser_find', { query: 'time' })
3241+
3242+
const clicked = await driver.executeTool('chat-test', 'browser_click', { elementId: 4 })
3243+
expect(clicked).toMatchObject({ ok: true, result: { dispatched: true } })
3244+
})
3245+
31013246
it.each([
31023247
['browser_snapshot', 'true'],
31033248
['browser_find', 'false'],
3104-
] as const)(
3105-
'passes the current root ref to %s and invalidates previous refs',
3106-
async (tool, markNew) => {
3107-
const contents = await openPage()
3108-
respondWith(contents, {
3109-
collectSnapshot: {
3110-
url: 'https://example.com/login',
3111-
title: 'Scoped',
3112-
scoped: true,
3113-
outline: '- button "Save" [ref=1]',
3114-
truncated: false,
3115-
refIds: [1],
3116-
refLineIndexes: { 1: 0 },
3117-
nextElementId: 2,
3118-
},
3119-
})
3120-
const result = await driver.executeTool('chat-test', tool, { elementId: 0, query: 'Save' })
3121-
expect(result).toMatchObject({ ok: true, result: { scoped: true } })
3122-
expect(
3123-
vi
3124-
.mocked(contents.executeJavaScript)
3125-
.mock.calls.some(
3126-
([expression]) =>
3127-
isPageCall(expression, 'collectSnapshot') &&
3128-
expression.includes(`.apply(null, [1,0,${markNew}])`)
3129-
)
3130-
).toBe(true)
3131-
const stale = await driver.executeTool('chat-test', 'browser_click', { elementId: 0 })
3132-
expect(stale).toMatchObject({ ok: false })
3133-
}
3134-
)
3249+
] as const)('passes the current root ref to %s', async (tool, markNew) => {
3250+
const contents = await openPage()
3251+
respondWith(contents, {
3252+
collectSnapshot: {
3253+
url: 'https://example.com/login',
3254+
title: 'Scoped',
3255+
scoped: true,
3256+
outline: '- button "Save" [ref=1]',
3257+
truncated: false,
3258+
refIds: [1],
3259+
refLineIndexes: { 1: 0 },
3260+
nextElementId: 2,
3261+
},
3262+
})
3263+
const result = await driver.executeTool('chat-test', tool, { elementId: 0, query: 'Save' })
3264+
expect(result).toMatchObject({ ok: true, result: { scoped: true } })
3265+
expect(
3266+
vi
3267+
.mocked(contents.executeJavaScript)
3268+
.mock.calls.some(
3269+
([expression]) =>
3270+
isPageCall(expression, 'collectSnapshot') &&
3271+
expression.includes(`.apply(null, [1,0,${markNew},`)
3272+
)
3273+
).toBe(true)
3274+
})
31353275

31363276
it.each([
31373277
{ kind: 'input:checkbox', checked: true, disabled: false, readOnly: false },

0 commit comments

Comments
 (0)