Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8282c98
feat(agent-mcp-interface): bootstrap package skeleton
DaniAkash Jun 12, 2026
4ea6fcd
chore: minor edits
DaniAkash Jun 12, 2026
02390dc
chore(agent-mcp-interface): scope fallow config to the package
DaniAkash Jun 12, 2026
a6809f5
Revert "chore(agent-mcp-interface): scope fallow config to the package"
DaniAkash Jun 12, 2026
71e854d
feat(agent-mcp-ui): bootstrap WXT extension surface
DaniAkash Jun 12, 2026
6f62d46
refactor(agent-mcp-ui): swap TanStack Router for react-router v7
DaniAkash Jun 12, 2026
f251067
feat(agent-mcp-ui): wire wxt dev launch into BrowserOS Chromium
DaniAkash Jun 12, 2026
dfabd49
feat(agent-mcp-ui): cockpit + new-agent + governance + live-run (#1193)
DaniAkash Jun 15, 2026
e641a9a
feat(agent-mcp-ui): replay + agents directory + mcp registry (#1221)
DaniAkash Jun 16, 2026
0f73944
feat(agent-mcp-ui): onboarding + governance permissions/site-rules/gr…
DaniAkash Jun 16, 2026
6a73122
chore: merge origin/main into feat/agent-mcp-interface-bootstrap
DaniAkash Jun 16, 2026
2853c50
feat(agent-mcp-interface): phase 1 — agent profiles CRUD end-to-end (…
DaniAkash Jun 17, 2026
89e6b26
feat(agent-mcp-interface): site rules + permissions catalog + check a…
DaniAkash Jun 17, 2026
ff7ed3b
feat(agent-mcp-interface): per-agent MCP server + executor stub (#1232)
DaniAkash Jun 17, 2026
bbcd448
feat(agent-mcp-interface): live integration polish + agent-mcp-manage…
DaniAkash Jun 17, 2026
a300f8d
feat(agent-mcp-interface): adopt real browser tools + mount inside ap…
DaniAkash Jun 18, 2026
0c3a46f
feat(agent-mcp-interface): attach to browseros browser over CDP at bo…
DaniAkash Jun 22, 2026
5d62153
chore(merge): sync feat/agent-mcp-interface-bootstrap with origin/main
DaniAkash Jun 22, 2026
6ab7e0b
chore: sync feat/agent-mcp-interface-bootstrap with origin/main
DaniAkash Jun 23, 2026
a6d2676
feat(agent-mcp-ui): single source for the MCP endpoint URL shown in a…
DaniAkash Jun 23, 2026
a928660
feat(cockpit): tab activity registry + homepage feedback loop (PR 1/3…
DaniAkash Jun 23, 2026
d3c9948
chore: sync feat/agent-mcp-interface-bootstrap with origin/main
DaniAkash Jun 23, 2026
9116a4e
fix(eval): restore any-typing on ai-sdk onStepFinish params for mixed…
DaniAkash Jun 23, 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
1 change: 1 addition & 0 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches:
- main
- dev
- feat/agent-mcp-interface-bootstrap
paths:
- "packages/browseros-agent/**"

Expand Down
1 change: 1 addition & 0 deletions packages/browseros-agent/.fallowrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"entry": [
"apps/server/src/index.ts",
"apps/server/src/compiled-bootstrap.ts",
"apps/agent-mcp-interface/src/main.ts",
"apps/agent/entrypoints/app/main.tsx",
"apps/agent/entrypoints/sidepanel/main.tsx",
"apps/agent/entrypoints/background/index.ts",
Expand Down
15 changes: 15 additions & 0 deletions packages/browseros-agent/apps/agent-mcp-interface/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
"root": false,
"extends": "//",
"linter": {
"rules": {
"suspicious": {
"noConsole": "error"
},
"style": {
"noProcessEnv": "error"
}
}
}
}
43 changes: 43 additions & 0 deletions packages/browseros-agent/apps/agent-mcp-interface/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@browseros/agent-mcp-interface",
"type": "module",
"private": true,
"version": "0.0.1",
"description": "Hono backend exposing the BrowserOS v2 agent surface: typed JSON API + per-agent MCP route mux. File-based config under <browserosDir>/mcp-interface/.",
"exports": {
"./server": {
"types": "./src/server.ts",
"default": null
},
"./cockpit": {
"types": "./src/cockpit.ts",
"default": "./src/cockpit.ts"
},
"./shared/port": {
"types": "./src/shared/port.ts",
"default": "./src/shared/port.ts"
}
},
"scripts": {
"start": "bun --watch src/main.ts",
"start:ci": "bun src/main.ts",
"test": "bun test",
"typecheck": "tsc --noEmit",
"lint": "bunx biome check",
"lint:fix": "bunx biome check --write --unsafe"
},
"dependencies": {
"@browseros/server": "workspace:*",
"@browseros/shared": "workspace:*",
"@hono/zod-validator": "^0.8.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"agent-mcp-manager": "^0.0.1",
"hono": "^4.12.3",
"nanoid": "^5.1.11",
"zod": "^4.4.3"
},
"devDependencies": {
"@types/bun": "^1.3.5",
"typescript": "^5.9.2"
}
}
84 changes: 84 additions & 0 deletions packages/browseros-agent/apps/agent-mcp-interface/src/cockpit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* The cockpit's "mount me inside apps/server" entry point.
*
* `createCockpitRoutes` is the function `apps/server`'s
* `createHttpServer` calls to splice the cockpit's `/agents`,
* `/site-rules`, `/permissions`, `/mcp/:slug`, and `/system` routes
* into the parent Hono runtime under a `/cockpit` prefix. The
* factory:
*
* 1. Wires the cockpit's process-wide singletons (the live
* `BrowserSession` for tool dispatch, and the base URL that
* `buildMcpUrl` uses to compose the per-agent endpoints).
* 2. Runs a one-shot `migrateMcpUrls` sweep over the profile
* directory so any URLs saved before the runtime merge get
* rewritten to the new shape; harness configs get re-installed
* automatically as part of the migration.
* 3. Returns the cockpit's Hono `app` so the parent can mount it
* via `.route('/cockpit', cockpitApp)`.
*
* The standalone `src/main.ts` still works for tests + solo dev
* (the BrowserSession stays null in that path, so tool dispatches
* short-circuit with "session not connected"). Production goes
* through `createCockpitRoutes`.
*/

import type { BrowserSession } from '@browseros/server/browser/core/session'
import { setBrowserSession } from './lib/browser-session'
import { logger } from './lib/logger'
import { migrateMcpUrls } from './lib/migrate-mcp-urls'
import { setLocalServerUrl } from './local-server-url'
import app from './server'

export interface CockpitDeps {
/** Live BrowserSession from the parent runtime. Tool dispatches bind to this. */
browserSession: BrowserSession
/** The port `apps/server` bound to; used to compose MCP URLs the harness can reach. */
serverPort: number
/**
* Mount prefix the cockpit's app is being routed under. Embedded in
* `buildMcpUrl` so per-agent URLs match the actual routable shape.
* Defaults to `/cockpit`; passing `''` removes the prefix entirely
* for testing.
*/
mountPrefix?: string
}

export function createCockpitRoutes(deps: CockpitDeps): typeof app {
const prefix = deps.mountPrefix ?? '/cockpit'
setBrowserSession(deps.browserSession)
setLocalServerUrl(`http://127.0.0.1:${deps.serverPort}${prefix}`)
// Fire the migration in the background; one bad profile must not
// block server startup. Result is logged so an operator can see
// how many rows moved.
void migrateMcpUrls(buildMcpUrlFromPort(deps.serverPort, prefix))
.then((result) =>
logger.info('mcpUrl migration finished', {
migrated: result.migrated,
skipped: result.skipped,
failed: result.failed,
}),
)
.catch((err: unknown) =>
// `migrateMcpUrls` guards per-profile errors, but `listFiles` at its
// start can still throw (EACCES, ENOTDIR, etc.). Without this `.catch`
// the rejection lands as an unhandled promise rejection that Bun
// discards silently. Logging keeps the failure visible to operators
// without preventing the rest of the cockpit from coming up.
logger.error('mcpUrl migration failed unexpectedly', {
error: err instanceof Error ? err.message : String(err),
}),
)
return app
}

function buildMcpUrlFromPort(
serverPort: number,
prefix: string,
): (slug: string) => string {
return (slug: string) => `http://127.0.0.1:${serverPort}${prefix}/mcp/${slug}`
}
63 changes: 63 additions & 0 deletions packages/browseros-agent/apps/agent-mcp-interface/src/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* Single chokepoint for env reads. Centralising here keeps the rest
* of the source free of process.env access and lets biome's
* noProcessEnv rule stay on at error level for every other file.
*/

import { COCKPIT_CDP_PORT_DEFAULT, PROD_API_PORT } from './shared/port'

function readPort(): number {
// biome-ignore lint/style/noProcessEnv: env.ts is the sanctioned env-reader for the package
const raw = process.env.BROWSEROS_AGENT_MCP_INTERFACE_PORT
if (!raw) return PROD_API_PORT
const parsed = Number(raw)
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
return PROD_API_PORT
}
return parsed
}

/**
* Port the cockpit dials when attaching to the BrowserOS Chromium
* over CDP. Default lives in IANA's dynamic / private range so it
* cannot collide with a registered service; the env override is the
* bridge until the BrowserOS browser shell defaults its DevTools
* port to the same value.
*/
function readCdpPort(): number {
// biome-ignore lint/style/noProcessEnv: env.ts is the sanctioned env-reader for the package
const raw = process.env.BROWSEROS_COCKPIT_CDP_PORT
if (!raw) return COCKPIT_CDP_PORT_DEFAULT
const parsed = Number(raw)
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
return COCKPIT_CDP_PORT_DEFAULT
}
return parsed
}

function readBrowserosDirOverride(): string | undefined {
// biome-ignore lint/style/noProcessEnv: env.ts is the sanctioned env-reader for the package
const raw = process.env.BROWSEROS_DIR?.trim()
return raw && raw.length > 0 ? raw : undefined
}

function readIsDevelopment(): boolean {
// biome-ignore lint/style/noProcessEnv: env.ts is the sanctioned env-reader for the package
return process.env.NODE_ENV === 'development'
}

/**
* Reads happen once at module load. Tests that need different values
* mutate this object before importing the rest of the source graph;
* production code treats it as immutable.
*/
export const env = {
port: readPort(),
cdpPort: readCdpPort(),
browserosDirOverride: readBrowserosDirOverride(),
isDevelopment: readIsDevelopment(),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* System-wide default approval catalog. This is the source for
* `GET /permissions/catalog` and the fallback `permissions.check()`
* uses when neither a site rule nor an agent verdict applies.
*
* Keep in sync with the UI's local catalog at
* apps/agent-mcp-ui/screens/new-agent/new-agent.schemas.ts
* (`APPROVAL_CATEGORIES`). The UI keeps its own copy as the
* fetch-failure fallback path for the Permissions tab; this server
* copy is the source of truth at the wire boundary.
*/

import { z } from 'zod'

export const approvalVerdictEnum = z.enum(['Auto', 'Ask', 'Block'])
export type ApprovalVerdict = z.infer<typeof approvalVerdictEnum>

export const approvalCategorySchema = z.object({
id: z.string(),
name: z.string(),
defaultVerdict: approvalVerdictEnum,
allowAuto: z.boolean(),
})
export type ApprovalCategory = z.infer<typeof approvalCategorySchema>

export const APPROVAL_CATEGORIES: readonly ApprovalCategory[] = [
{
id: 'submit',
name: 'Submit / send / post',
defaultVerdict: 'Ask',
allowAuto: true,
},
{
id: 'payment',
name: 'Payments & checkout',
defaultVerdict: 'Block',
allowAuto: false,
},
{
id: 'delete',
name: 'Delete / destructive',
defaultVerdict: 'Ask',
allowAuto: true,
},
{ id: 'upload', name: 'File upload', defaultVerdict: 'Ask', allowAuto: true },
{
id: 'navigate',
name: 'Navigate to a new site',
defaultVerdict: 'Ask',
allowAuto: true,
},
{
id: 'input',
name: 'Click & type',
defaultVerdict: 'Auto',
allowAuto: true,
},
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license
* Copyright 2025 BrowserOS
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* In-process async mutex. Tasks queued via `run` execute one at a
* time in submission order; subsequent tasks always run, regardless
* of whether the previous one resolved or rejected.
*
* Used by the agents service to serialise slug-mutating operations
* (create, update, regenerateMcpUrl) so the read-snapshot → compute
* → write pattern cannot race against itself. The interface server
* is a single-process loopback bind so per-process serialisation is
* the right granularity; multi-process scaling would warrant a
* filesystem-level guard (O_EXCL on a lockfile) instead.
*/

export class AsyncMutex {
private chain: Promise<unknown> = Promise.resolve()

run<T>(task: () => Promise<T>): Promise<T> {
// Swallow any previous rejection so the next task always runs;
// the caller of the prior task already saw the rejection on its
// own promise.
const next = this.chain.catch(() => undefined).then(task)
this.chain = next.catch(() => undefined)
return next
}
}
Loading
Loading