Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
392dc44
feat(aws-marketplace): sell several listings from one integration thr…
waleedlatif1 Nov 12, 2026
b01becc
improvement(changelog): cover meaningful product updates through v0.9…
waleedlatif1 Nov 12, 2026
d87daa7
fix(api): report a finished run's real status once its pauses have se…
waleedlatif1 Nov 12, 2026
3e5200f
feat(apollo): add optional OAuth connections (#9922)
waleedlatif1 Nov 12, 2026
d5e98fa
feat(credentials): regional and instance hosts for saved LaunchDarkly…
waleedlatif1 Nov 12, 2026
7288b2d
feat(bedrock): run Bedrock models with a saved AWS credential (#9932)
waleedlatif1 Nov 12, 2026
0d77c23
feat(credentials): record, show, and explain saved token expiry (#9930)
waleedlatif1 Nov 12, 2026
7d5641d
feat(credentials): keyless Google Workspace domain-wide delegation vi…
waleedlatif1 Nov 12, 2026
bcadf23
feat(credentials): Microsoft app credentials for Power BI and Azure D…
waleedlatif1 Nov 12, 2026
4911dc1
improvement(credentials): keep valid client-credential tokens through…
waleedlatif1 Nov 12, 2026
ff166c1
feat(credentials): saved credentials for Amplitude, Kalshi, Splunk, C…
waleedlatif1 Nov 12, 2026
e5af324
feat(knowledge): let the S3 connector sync with a saved AWS credentia…
waleedlatif1 Nov 12, 2026
21e32d7
feat(credentials): second auth modes for ServiceNow, Snowflake, Infis…
waleedlatif1 Nov 12, 2026
ef7bc52
feat(credentials): saved PostgreSQL, MySQL, and SQL Server connection…
waleedlatif1 Nov 12, 2026
3373438
feat(aws-marketplace): bill a listing's LLM usage to AWS through a pe…
waleedlatif1 Nov 12, 2026
a8fbd58
docs(library): update enterprise-ai-agent-compliance-deployment-guide…
waleedlatif1 Nov 12, 2026
a87450b
docs(library): update ai-agent-orchestration-frameworks-explained (#9…
waleedlatif1 Nov 12, 2026
4983632
docs(library): update govern-ai-agents-multiple-teams-enterprise-work…
waleedlatif1 Nov 12, 2026
a6335d8
feat(library): AI Agent Platform Vendor Viability: Funding, Adoption,…
waleedlatif1 Nov 12, 2026
309539e
feat(library): How to Build a Shopify AI Agent for Orders and Invento…
waleedlatif1 Nov 12, 2026
f4dcf73
fix(credentials): Splunk password sign-in, Snowflake key pair in the …
waleedlatif1 Nov 12, 2026
f961415
docs(library): remove duplicate questions in orchestration and govern…
waleedlatif1 Nov 12, 2026
cff97b2
fix(mothership): back off the transport poll instead of retrying ever…
waleedlatif1 Nov 12, 2026
2857279
fix(google): validate Workspace integrations, return dropped fields a…
waleedlatif1 Nov 12, 2026
15b2a42
feat(computer-use): enable computer use by default, governed by a per…
waleedlatif1 Nov 12, 2026
8fbc750
improvement(aws-marketplace): close billing gaps and simplify the lic…
waleedlatif1 Nov 12, 2026
0395eb3
revert(library): restore orchestration and governance posts to before…
waleedlatif1 Nov 12, 2026
51e21ae
improvement(desktop-browser): stable element refs, wait observations,…
waleedlatif1 Nov 12, 2026
4a5b2c6
fix(google): close post-merge verification gaps from the Workspace sw…
waleedlatif1 Nov 12, 2026
143fe02
fix(microsoft): validate Microsoft integrations, return dropped field…
waleedlatif1 Nov 12, 2026
cc012bf
feat(gitlab): add permission CSV exporter (#9955)
waleedlatif1 Nov 12, 2026
9866b65
fix(atlassian): validate Jira, Confluence, JSM, Bitbucket, Trello aga…
waleedlatif1 Nov 12, 2026
876fc72
improvement(desktop-browser): click through a nested control that fil…
waleedlatif1 Nov 12, 2026
4003d19
fix(aws): validate AWS integrations against SDK and API docs (#9952)
waleedlatif1 Nov 12, 2026
364d1f4
chore(api): describe the settled-pause rule in the run status overlay…
waleedlatif1 Nov 12, 2026
4388745
fix(gitlab): require confirmed emails in permission exports (#9958)
waleedlatif1 Nov 12, 2026
82f8c35
fix(desktop-browser): refuse reissued earlier refs and recover from u…
waleedlatif1 Nov 12, 2026
8f2bd0b
fix(integrations): close stale and empty value leaks from the Google …
waleedlatif1 Nov 12, 2026
b266bd9
fix(integrations): final review pass for the Atlassian and AWS sweeps…
waleedlatif1 Nov 12, 2026
55bacfc
feat(credentials): configurable audience for keyless AWS, Databricks,…
waleedlatif1 Nov 13, 2026
005cb97
fix(tests): close three integration-test races that flaked CI (#9964)
waleedlatif1 Nov 13, 2026
34b6101
fix(integrations): return the Dataverse page size and correct Contact…
waleedlatif1 Nov 13, 2026
9c1857c
fix(executor): resolve saved model credentials inside each fallback a…
waleedlatif1 Nov 13, 2026
016755f
chore(integrations): tidy two comments left by the Google/Microsoft s…
waleedlatif1 Nov 13, 2026
140b92c
fix(desktop-browser): give a wait's closing snapshot a full budget an…
waleedlatif1 Nov 13, 2026
c79ff1a
fix(aws): round-trip typed DynamoDB cursors and allow percentile-only…
waleedlatif1 Nov 13, 2026
85b731a
fix(credentials): point the AWS keyless hint at the policy audience, …
waleedlatif1 Nov 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ jobs:
working-directory: packages/db
run: |
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()'
# A test that hangs on a lock leaves only its timeout behind; these name the waiter, the
# lock, and the blocking statement in the Postgres log. Service containers take no -c flags,
# and ALTER SYSTEM runs outside a transaction, so each setting is its own statement.
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()'
bun run db:${{ matrix.provision }}
- name: Verify migration replay is a no-op
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/docs-negotiation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ jobs:
DOCS_GENERATORS_REPORT_PATH: ${{ github.workspace }}/apps/docs/test-results/generators/report.json
DOCS_COMPATIBILITY_REPORT_PATH: ${{ github.workspace }}/apps/docs/test-results/compatibility.json
DOCS_HEADER_REPORT_PATH: ${{ github.workspace }}/apps/docs/test-results/docs-headers.json
GITLAB_EXPORT_REPORT_PATH: ${{ github.workspace }}/apps/docs/test-results/gitlab-export.json
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
Expand All @@ -292,6 +293,8 @@ jobs:
- name: Verify native header edge cases
if: ${{ !cancelled() && steps.generators.outcome != 'skipped' }}
run: bun run --cwd apps/docs test:headers:e2e
- name: Verify GitLab CSV export over HTTPS
run: python3 apps/docs/scripts/test-gitlab-export-e2e.py
- name: Bound the generated content cache
if: always()
run: bash .github/actions/docs-content-cache/prune.sh content
Expand Down
9 changes: 4 additions & 5 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,10 @@ Cadence: Electron ships a major every ~8 weeks and supports the latest 3 — bud

## Native Mac computer use

Mothership's `computer` tool is available on macOS 14+ when the global runtime flag
`mothership-computer-use` is enabled and the user opts in under **Settings → Desktop → Computer Use**.
The flag defaults off; local/self-hosted deployments use `MSHIP_COMPUTER_USE=true`, while hosted
rollout uses the existing AppConfig feature-flags document. Older desktop builds omit this optional
bridge and continue to work.
Mothership's `computer` tool is available on macOS 14+ once the user opts in under
**Settings → Desktop → Computer Use**. Enterprise organizations can withhold it from a permission
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.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/e2e/browser-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1365,7 +1365,7 @@ test.describe('browser tools', () => {
expect(await formState()).toMatchObject({ name: 'Observed value' })
const result = fill.result as { observation: { result: { matches: { elementId: number }[] } } }
const freshId = result.observation.result.matches[0].elementId
expect(freshId).not.toBe(ref('Name'))
expect(freshId).toBe(ref('Name'))
const clear = await execute('browser_type', { elementId: freshId, text: '', observe: {} })
expect(clear.ok, clear.error).toBe(true)
expect(clear.result).toMatchObject({ observation: { ok: true } })
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/e2e/local-files.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ test('native file tools remember folder consent across chats and restarts until
stdin: {
contents: `import { createRoot } from 'react-dom/client';
import { ToastProvider } from '@sim/emcn';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AppRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime';
import { PathParamsContext } from 'next/dist/shared/lib/hooks-client-context.shared-runtime';
import { Desktop } from '@/app/workspace/[workspaceId]/settings/components/desktop/desktop';
Expand All @@ -70,11 +71,11 @@ const router = { bfcacheId: 'fixture', back: () => history.back(), forward: () =
createRoot(document.getElementById('settings')).render(
<AppRouterContext.Provider value={router}>
<PathParamsContext.Provider value={{ workspaceId: 'fixture' }}>
<ToastProvider><SettingsHeaderProvider><SettingsHeaderShell>
<QueryClientProvider client={new QueryClient({ defaultOptions: { queries: { retry: false } } })}><ToastProvider><SettingsHeaderProvider><SettingsHeaderShell>
<FeatureFlagsProvider flags={{ dashboards: false, 'workflow-tests': false, 'mothership-model-selector': false, 'mothership-plan-mode': false }}>
<SettingsSectionProvider plane='workspace' section='desktop'><Desktop /></SettingsSectionProvider>
</FeatureFlagsProvider>
</SettingsHeaderShell></SettingsHeaderProvider></ToastProvider>
</SettingsHeaderShell></SettingsHeaderProvider></ToastProvider></QueryClientProvider>
</PathParamsContext.Provider>
</AppRouterContext.Provider>
);`,
Expand Down
216 changes: 178 additions & 38 deletions apps/desktop/src/main/browser-agent/driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1507,13 +1507,13 @@ describe('browserToolWatchdogMs', () => {
})

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

Expand Down Expand Up @@ -3098,40 +3098,180 @@ describe('credential protection', () => {
})
})

it('gives a wait that runs to its timeout a full snapshot budget for its closing observation', async () => {
const contents = await openPage()
let finishSnapshot: (value: unknown) => void = () => {}
vi.mocked(contents.executeJavaScript).mockImplementation((expression: string) => {
if (!isPageCall(expression, 'collectSnapshot')) return Promise.resolve(undefined)
return new Promise((resolve) => {
finishSnapshot = resolve
})
})
vi.useFakeTimers()
try {
const waiting = driver.executeTool('chat-test', 'browser_wait_for', { timeoutMs: 1_000 })
await vi.advanceTimersByTimeAsync(9_000)
finishSnapshot({
url: 'https://example.com/',
title: 'Example',
outline: '- heading "Order confirmed"',
truncated: false,
refIds: [],
refLineIndexes: {},
nextElementId: 1_000,
})
await vi.advanceTimersByTimeAsync(1_000)

expect(await waiting).toMatchObject({
ok: true,
result: { observation: { ok: true, result: { outline: '- heading "Order confirmed"' } } },
})
} finally {
vi.useRealTimers()
}
})

it('accepts a later snapshot that keeps refs this scope issued for the same page', async () => {
const contents = await openPage()
const snapshot = (refIds: number[], nextElementId: number) => ({
url: 'https://example.com/',
title: 'Example',
outline: refIds.map((ref) => `- button "B${ref}" [ref=${ref}]`).join('\n'),
truncated: false,
refIds,
refLineIndexes: Object.fromEntries(refIds.map((ref, index) => [ref, index])),
nextElementId,
})
respondWith(contents, { collectSnapshot: snapshot([4, 5], 6) })
await driver.executeTool('chat-test', 'browser_snapshot', {})
respondWith(contents, { collectSnapshot: snapshot([4, 6], 7) })

const kept = await driver.executeTool('chat-test', 'browser_snapshot', {})
expect(kept).toMatchObject({ ok: true })
respondWith(contents, { collectSnapshot: snapshot([2], 7) })

const forged = await driver.executeTool('chat-test', 'browser_snapshot', {})
expect(forged).toMatchObject({ ok: false })
})

it("forgets a page's oldest refs once its history passes the retention bound", async () => {
const contents = await openPage()
const snapshot = (refIds: number[], nextElementId: number) => ({
url: 'https://example.com/',
title: 'Example',
outline: refIds.map((ref) => `- button "B${ref}" [ref=${ref}]`).join('\n'),
truncated: false,
refIds,
refLineIndexes: Object.fromEntries(refIds.map((ref, index) => [ref, index])),
nextElementId,
})
let next = 0
for (let round = 0; round < 19; round++) {
const refIds = Array.from({ length: 300 }, (_, index) => next + index)
next += 300
respondWith(contents, { collectSnapshot: snapshot(refIds, next) })
expect(await driver.executeTool('chat-test', 'browser_snapshot', {})).toMatchObject({
ok: true,
})
}
respondWith(contents, { collectSnapshot: snapshot([next - 1], next) })
expect(await driver.executeTool('chat-test', 'browser_snapshot', {})).toMatchObject({
ok: true,
})
respondWith(contents, { collectSnapshot: snapshot([0], next) })

expect(await driver.executeTool('chat-test', 'browser_snapshot', {})).toMatchObject({
ok: false,
})
})

it('recaptures with fresh ids when the page reuses an id this scope never committed', async () => {
const contents = await openPage()
const snapshot = (refIds: number[], nextElementId: number) => ({
url: 'https://example.com/',
title: 'Example',
outline: refIds.map((ref) => `- button "B${ref}" [ref=${ref}]`).join('\n'),
truncated: false,
refIds,
refLineIndexes: Object.fromEntries(refIds.map((ref, index) => [ref, index])),
nextElementId,
})
respondWith(contents, { collectSnapshot: snapshot([4, 5], 6) })
await driver.executeTool('chat-test', 'browser_snapshot', {})
vi.mocked(contents.executeJavaScript).mockImplementation((expression: string) => {
if (!isPageCall(expression, 'collectSnapshot')) return Promise.resolve(undefined)
const forgets = expression.includes(',true])')
return Promise.resolve(forgets ? snapshot([6, 7], 8) : snapshot([2, 6], 7))
})

const result = await driver.executeTool('chat-test', 'browser_snapshot', {})

expect(result).toMatchObject({
ok: true,
result: { outline: expect.stringContaining('[ref=7]') },
})
})

it('acts on a ref an earlier snapshot of the same page issued', async () => {
const contents = await openPage()
respondWith(contents, {
collectSnapshot: {
url: 'https://example.com/',
title: 'Example',
outline: '- button "Pins" [ref=4]\n- button "Files" [ref=5]',
truncated: false,
refIds: [4, 5],
refLineIndexes: { 4: 0, 5: 1 },
nextElementId: 6,
},
})
await driver.executeTool('chat-test', 'browser_snapshot', {})
respondWith(contents, {
collectSnapshot: {
url: 'https://example.com/',
title: 'Example',
outline: '- dialog "Pick a time" [ref=6]',
truncated: false,
refIds: [6],
refLineIndexes: { 6: 0 },
nextElementId: 7,
},
})
await driver.executeTool('chat-test', 'browser_find', { query: 'time' })

const clicked = await driver.executeTool('chat-test', 'browser_click', { elementId: 4 })
expect(clicked).toMatchObject({ ok: true, result: { dispatched: true } })
})

it.each([
['browser_snapshot', 'true'],
['browser_find', 'false'],
] as const)(
'passes the current root ref to %s and invalidates previous refs',
async (tool, markNew) => {
const contents = await openPage()
respondWith(contents, {
collectSnapshot: {
url: 'https://example.com/login',
title: 'Scoped',
scoped: true,
outline: '- button "Save" [ref=1]',
truncated: false,
refIds: [1],
refLineIndexes: { 1: 0 },
nextElementId: 2,
},
})
const result = await driver.executeTool('chat-test', tool, { elementId: 0, query: 'Save' })
expect(result).toMatchObject({ ok: true, result: { scoped: true } })
expect(
vi
.mocked(contents.executeJavaScript)
.mock.calls.some(
([expression]) =>
isPageCall(expression, 'collectSnapshot') &&
expression.includes(`.apply(null, [1,0,${markNew}])`)
)
).toBe(true)
const stale = await driver.executeTool('chat-test', 'browser_click', { elementId: 0 })
expect(stale).toMatchObject({ ok: false })
}
)
] as const)('passes the current root ref to %s', async (tool, markNew) => {
const contents = await openPage()
respondWith(contents, {
collectSnapshot: {
url: 'https://example.com/login',
title: 'Scoped',
scoped: true,
outline: '- button "Save" [ref=1]',
truncated: false,
refIds: [1],
refLineIndexes: { 1: 0 },
nextElementId: 2,
},
})
const result = await driver.executeTool('chat-test', tool, { elementId: 0, query: 'Save' })
expect(result).toMatchObject({ ok: true, result: { scoped: true } })
expect(
vi
.mocked(contents.executeJavaScript)
.mock.calls.some(
([expression]) =>
isPageCall(expression, 'collectSnapshot') &&
expression.includes(`.apply(null, [1,0,${markNew},`)
)
).toBe(true)
})

it.each([
{ kind: 'input:checkbox', checked: true, disabled: false, readOnly: false },
Expand Down
Loading
Loading