@agenticmail/enterprise 0.5.78 → 0.5.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-7RNT4O5T.js +15198 -0
- package/dist/chunk-AGFOJCSB.js +2191 -0
- package/dist/chunk-F4GSFCM3.js +898 -0
- package/dist/chunk-PZA7YOJE.js +898 -0
- package/dist/chunk-Q3V7VZFQ.js +2191 -0
- package/dist/chunk-RRFB6G6M.js +15198 -0
- package/dist/chunk-VX3VFMVB.js +409 -0
- package/dist/cli.js +1 -1
- package/dist/dashboard/pages/agent-detail.js +313 -1
- package/dist/index.js +4 -3
- package/dist/pw-ai-KPETTB25.js +2212 -0
- package/dist/routes-PDHMCIXU.js +6676 -0
- package/dist/runtime-7HW4GX5L.js +48 -0
- package/dist/runtime-XXDCZZIK.js +48 -0
- package/dist/server-FMP4BFGW.js +12 -0
- package/dist/server-JRHDUNII.js +12 -0
- package/dist/setup-O5FPRLK4.js +20 -0
- package/dist/setup-S4Z4PPIJ.js +20 -0
- package/package.json +15 -2
- package/src/agent-tools/common.ts +25 -0
- package/src/agent-tools/index.ts +3 -0
- package/src/agent-tools/schema/typebox.ts +25 -0
- package/src/agent-tools/tools/browser-tool.schema.ts +112 -0
- package/src/agent-tools/tools/browser-tool.ts +388 -0
- package/src/agent-tools/tools/gateway.ts +126 -0
- package/src/agent-tools/tools/nodes-utils.ts +80 -0
- package/src/browser/bridge-auth-registry.ts +34 -0
- package/src/browser/bridge-server.ts +93 -0
- package/src/browser/cdp.helpers.ts +180 -0
- package/src/browser/cdp.ts +466 -0
- package/src/browser/chrome.executables.ts +625 -0
- package/src/browser/chrome.profile-decoration.ts +198 -0
- package/src/browser/chrome.ts +349 -0
- package/src/browser/client-actions-core.ts +259 -0
- package/src/browser/client-actions-observe.ts +184 -0
- package/src/browser/client-actions-state.ts +284 -0
- package/src/browser/client-actions-types.ts +16 -0
- package/src/browser/client-actions-url.ts +11 -0
- package/src/browser/client-actions.ts +4 -0
- package/src/browser/client-fetch.ts +253 -0
- package/src/browser/client.ts +337 -0
- package/src/browser/config.ts +296 -0
- package/src/browser/constants.ts +8 -0
- package/src/browser/control-auth.ts +94 -0
- package/src/browser/control-service.ts +81 -0
- package/src/browser/csrf.ts +87 -0
- package/src/browser/enterprise-compat.ts +518 -0
- package/src/browser/extension-relay.ts +834 -0
- package/src/browser/http-auth.ts +63 -0
- package/src/browser/navigation-guard.ts +50 -0
- package/src/browser/paths.ts +49 -0
- package/src/browser/profiles-service.ts +187 -0
- package/src/browser/profiles.ts +113 -0
- package/src/browser/proxy-files.ts +41 -0
- package/src/browser/pw-ai-module.ts +52 -0
- package/src/browser/pw-ai-state.ts +9 -0
- package/src/browser/pw-ai.ts +65 -0
- package/src/browser/pw-role-snapshot.ts +434 -0
- package/src/browser/pw-session.ts +810 -0
- package/src/browser/pw-tools-core.activity.ts +68 -0
- package/src/browser/pw-tools-core.downloads.ts +281 -0
- package/src/browser/pw-tools-core.interactions.ts +646 -0
- package/src/browser/pw-tools-core.responses.ts +124 -0
- package/src/browser/pw-tools-core.shared.ts +70 -0
- package/src/browser/pw-tools-core.snapshot.ts +213 -0
- package/src/browser/pw-tools-core.state.ts +209 -0
- package/src/browser/pw-tools-core.storage.ts +128 -0
- package/src/browser/pw-tools-core.trace.ts +37 -0
- package/src/browser/pw-tools-core.ts +8 -0
- package/src/browser/resolved-config-refresh.ts +59 -0
- package/src/browser/routes/agent.act.shared.ts +52 -0
- package/src/browser/routes/agent.act.ts +575 -0
- package/src/browser/routes/agent.debug.ts +149 -0
- package/src/browser/routes/agent.shared.ts +143 -0
- package/src/browser/routes/agent.snapshot.ts +333 -0
- package/src/browser/routes/agent.storage.ts +451 -0
- package/src/browser/routes/agent.ts +13 -0
- package/src/browser/routes/basic.ts +202 -0
- package/src/browser/routes/dispatcher.ts +126 -0
- package/src/browser/routes/index.ts +11 -0
- package/src/browser/routes/path-output.ts +1 -0
- package/src/browser/routes/tabs.ts +217 -0
- package/src/browser/routes/types.ts +26 -0
- package/src/browser/routes/utils.ts +73 -0
- package/src/browser/screenshot.ts +54 -0
- package/src/browser/server-context.ts +688 -0
- package/src/browser/server-context.types.ts +65 -0
- package/src/browser/server-lifecycle.ts +48 -0
- package/src/browser/server-middleware.ts +37 -0
- package/src/browser/server.ts +110 -0
- package/src/browser/target-id.ts +30 -0
- package/src/browser/trash.ts +21 -0
- package/src/dashboard/pages/agent-detail.js +313 -1
- package/src/engine/agent-routes.ts +46 -0
- package/src/security/external-content.ts +299 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Server } from "node:http";
|
|
2
|
+
import type { RunningChrome } from "./chrome.js";
|
|
3
|
+
import type { BrowserTab } from "./client.js";
|
|
4
|
+
import type { ResolvedBrowserConfig, ResolvedBrowserProfile } from "./config.js";
|
|
5
|
+
|
|
6
|
+
export type { BrowserTab };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Runtime state for a single profile's Chrome instance.
|
|
10
|
+
*/
|
|
11
|
+
export type ProfileRuntimeState = {
|
|
12
|
+
profile: ResolvedBrowserProfile;
|
|
13
|
+
running: RunningChrome | null;
|
|
14
|
+
/** Sticky tab selection when callers omit targetId (keeps snapshot+act consistent). */
|
|
15
|
+
lastTargetId?: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type BrowserServerState = {
|
|
19
|
+
server?: Server | null;
|
|
20
|
+
port: number;
|
|
21
|
+
resolved: ResolvedBrowserConfig;
|
|
22
|
+
profiles: Map<string, ProfileRuntimeState>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type BrowserProfileActions = {
|
|
26
|
+
ensureBrowserAvailable: () => Promise<void>;
|
|
27
|
+
ensureTabAvailable: (targetId?: string) => Promise<BrowserTab>;
|
|
28
|
+
isHttpReachable: (timeoutMs?: number) => Promise<boolean>;
|
|
29
|
+
isReachable: (timeoutMs?: number) => Promise<boolean>;
|
|
30
|
+
listTabs: () => Promise<BrowserTab[]>;
|
|
31
|
+
openTab: (url: string) => Promise<BrowserTab>;
|
|
32
|
+
focusTab: (targetId: string) => Promise<void>;
|
|
33
|
+
closeTab: (targetId: string) => Promise<void>;
|
|
34
|
+
stopRunningBrowser: () => Promise<{ stopped: boolean }>;
|
|
35
|
+
resetProfile: () => Promise<{ moved: boolean; from: string; to?: string }>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type BrowserRouteContext = {
|
|
39
|
+
state: () => BrowserServerState;
|
|
40
|
+
forProfile: (profileName?: string) => ProfileContext;
|
|
41
|
+
listProfiles: () => Promise<ProfileStatus[]>;
|
|
42
|
+
// Legacy methods delegate to default profile for backward compatibility
|
|
43
|
+
mapTabError: (err: unknown) => { status: number; message: string } | null;
|
|
44
|
+
} & BrowserProfileActions;
|
|
45
|
+
|
|
46
|
+
export type ProfileContext = {
|
|
47
|
+
profile: ResolvedBrowserProfile;
|
|
48
|
+
} & BrowserProfileActions;
|
|
49
|
+
|
|
50
|
+
export type ProfileStatus = {
|
|
51
|
+
name: string;
|
|
52
|
+
cdpPort: number;
|
|
53
|
+
cdpUrl: string;
|
|
54
|
+
color: string;
|
|
55
|
+
running: boolean;
|
|
56
|
+
tabCount: number;
|
|
57
|
+
isDefault: boolean;
|
|
58
|
+
isRemote: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type ContextOptions = {
|
|
62
|
+
getState: () => BrowserServerState | null;
|
|
63
|
+
onEnsureAttachTarget?: (profile: ResolvedBrowserProfile) => Promise<void>;
|
|
64
|
+
refreshConfigFromDisk?: boolean;
|
|
65
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ResolvedBrowserConfig } from "./config.js";
|
|
2
|
+
import { resolveProfile } from "./config.js";
|
|
3
|
+
import { ensureChromeExtensionRelayServer } from "./extension-relay.js";
|
|
4
|
+
import {
|
|
5
|
+
type BrowserServerState,
|
|
6
|
+
createBrowserRouteContext,
|
|
7
|
+
listKnownProfileNames,
|
|
8
|
+
} from "./server-context.js";
|
|
9
|
+
|
|
10
|
+
export async function ensureExtensionRelayForProfiles(params: {
|
|
11
|
+
resolved: ResolvedBrowserConfig;
|
|
12
|
+
onWarn: (message: string) => void;
|
|
13
|
+
}) {
|
|
14
|
+
for (const name of Object.keys(params.resolved.profiles)) {
|
|
15
|
+
const profile = resolveProfile(params.resolved, name);
|
|
16
|
+
if (!profile || profile.driver !== "extension") {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch((err) => {
|
|
20
|
+
params.onWarn(`Chrome extension relay init failed for profile "${name}": ${String(err)}`);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function stopKnownBrowserProfiles(params: {
|
|
26
|
+
getState: () => BrowserServerState | null;
|
|
27
|
+
onWarn: (message: string) => void;
|
|
28
|
+
}) {
|
|
29
|
+
const current = params.getState();
|
|
30
|
+
if (!current) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const ctx = createBrowserRouteContext({
|
|
34
|
+
getState: params.getState,
|
|
35
|
+
refreshConfigFromDisk: true,
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
for (const name of listKnownProfileNames(current)) {
|
|
39
|
+
try {
|
|
40
|
+
await ctx.forProfile(name).stopRunningBrowser();
|
|
41
|
+
} catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
params.onWarn(`openclaw browser stop failed: ${String(err)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { browserMutationGuardMiddleware } from "./csrf.js";
|
|
4
|
+
import { isAuthorizedBrowserRequest } from "./http-auth.js";
|
|
5
|
+
|
|
6
|
+
export function installBrowserCommonMiddleware(app: Express) {
|
|
7
|
+
app.use((req, res, next) => {
|
|
8
|
+
const ctrl = new AbortController();
|
|
9
|
+
const abort = () => ctrl.abort(new Error("request aborted"));
|
|
10
|
+
req.once("aborted", abort);
|
|
11
|
+
res.once("close", () => {
|
|
12
|
+
if (!res.writableEnded) {
|
|
13
|
+
abort();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
// Make the signal available to browser route handlers (best-effort).
|
|
17
|
+
(req as unknown as { signal?: AbortSignal }).signal = ctrl.signal;
|
|
18
|
+
next();
|
|
19
|
+
});
|
|
20
|
+
app.use(express.json({ limit: "1mb" }));
|
|
21
|
+
app.use(browserMutationGuardMiddleware());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function installBrowserAuthMiddleware(
|
|
25
|
+
app: Express,
|
|
26
|
+
auth: { token?: string; password?: string },
|
|
27
|
+
) {
|
|
28
|
+
if (!auth.token && !auth.password) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
app.use((req, res, next) => {
|
|
32
|
+
if (isAuthorizedBrowserRequest(req, auth)) {
|
|
33
|
+
return next();
|
|
34
|
+
}
|
|
35
|
+
res.status(401).send("Unauthorized");
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Server } from "node:http";
|
|
2
|
+
import express from "express";
|
|
3
|
+
|
|
4
|
+
import { resolveBrowserConfig } from "./config.js";
|
|
5
|
+
import { ensureBrowserControlAuth, resolveBrowserControlAuth } from "./control-auth.js";
|
|
6
|
+
import { isPwAiLoaded } from "./pw-ai-state.js";
|
|
7
|
+
import { registerBrowserRoutes } from "./routes/index.js";
|
|
8
|
+
import type { BrowserRouteRegistrar } from "./routes/types.js";
|
|
9
|
+
import { type BrowserServerState, createBrowserRouteContext } from "./server-context.js";
|
|
10
|
+
import { ensureExtensionRelayForProfiles, stopKnownBrowserProfiles } from "./server-lifecycle.js";
|
|
11
|
+
import {
|
|
12
|
+
import { createSubsystemLogger, loadConfig } from "./enterprise-compat.js";
|
|
13
|
+
installBrowserAuthMiddleware,
|
|
14
|
+
installBrowserCommonMiddleware,
|
|
15
|
+
} from "./server-middleware.js";
|
|
16
|
+
|
|
17
|
+
let state: BrowserServerState | null = null;
|
|
18
|
+
const log = createSubsystemLogger("browser");
|
|
19
|
+
const logServer = log.child("server");
|
|
20
|
+
|
|
21
|
+
export async function startBrowserControlServerFromConfig(): Promise<BrowserServerState | null> {
|
|
22
|
+
if (state) {
|
|
23
|
+
return state;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const cfg = loadConfig();
|
|
27
|
+
const resolved = resolveBrowserConfig(cfg.browser, cfg);
|
|
28
|
+
if (!resolved.enabled) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let browserAuth = resolveBrowserControlAuth(cfg);
|
|
33
|
+
try {
|
|
34
|
+
const ensured = await ensureBrowserControlAuth({ cfg });
|
|
35
|
+
browserAuth = ensured.auth;
|
|
36
|
+
if (ensured.generatedToken) {
|
|
37
|
+
logServer.info("No browser auth configured; generated gateway.auth.token automatically.");
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
logServer.warn(`failed to auto-configure browser auth: ${String(err)}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const app = express();
|
|
44
|
+
installBrowserCommonMiddleware(app);
|
|
45
|
+
installBrowserAuthMiddleware(app, browserAuth);
|
|
46
|
+
|
|
47
|
+
const ctx = createBrowserRouteContext({
|
|
48
|
+
getState: () => state,
|
|
49
|
+
refreshConfigFromDisk: true,
|
|
50
|
+
});
|
|
51
|
+
registerBrowserRoutes(app as unknown as BrowserRouteRegistrar, ctx);
|
|
52
|
+
|
|
53
|
+
const port = resolved.controlPort;
|
|
54
|
+
const server = await new Promise<Server>((resolve, reject) => {
|
|
55
|
+
const s = app.listen(port, "127.0.0.1", () => resolve(s));
|
|
56
|
+
s.once("error", reject);
|
|
57
|
+
}).catch((err) => {
|
|
58
|
+
logServer.error(`openclaw browser server failed to bind 127.0.0.1:${port}: ${String(err)}`);
|
|
59
|
+
return null;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!server) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
state = {
|
|
67
|
+
server,
|
|
68
|
+
port,
|
|
69
|
+
resolved,
|
|
70
|
+
profiles: new Map(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await ensureExtensionRelayForProfiles({
|
|
74
|
+
resolved,
|
|
75
|
+
onWarn: (message) => logServer.warn(message),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const authMode = browserAuth.token ? "token" : browserAuth.password ? "password" : "off";
|
|
79
|
+
logServer.info(`Browser control listening on http://127.0.0.1:${port}/ (auth=${authMode})`);
|
|
80
|
+
return state;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function stopBrowserControlServer(): Promise<void> {
|
|
84
|
+
const current = state;
|
|
85
|
+
if (!current) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await stopKnownBrowserProfiles({
|
|
90
|
+
getState: () => state,
|
|
91
|
+
onWarn: (message) => logServer.warn(message),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (current.server) {
|
|
95
|
+
await new Promise<void>((resolve) => {
|
|
96
|
+
current.server?.close(() => resolve());
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
state = null;
|
|
100
|
+
|
|
101
|
+
// Optional: avoid importing heavy Playwright bridge when this process never used it.
|
|
102
|
+
if (isPwAiLoaded()) {
|
|
103
|
+
try {
|
|
104
|
+
const mod = await import("./pw-ai.js");
|
|
105
|
+
await mod.closePlaywrightBrowserConnection();
|
|
106
|
+
} catch {
|
|
107
|
+
// ignore
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type TargetIdResolution =
|
|
2
|
+
| { ok: true; targetId: string }
|
|
3
|
+
| { ok: false; reason: "not_found" | "ambiguous"; matches?: string[] };
|
|
4
|
+
|
|
5
|
+
export function resolveTargetIdFromTabs(
|
|
6
|
+
input: string,
|
|
7
|
+
tabs: Array<{ targetId: string }>,
|
|
8
|
+
): TargetIdResolution {
|
|
9
|
+
const needle = input.trim();
|
|
10
|
+
if (!needle) {
|
|
11
|
+
return { ok: false, reason: "not_found" };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const exact = tabs.find((t) => t.targetId === needle);
|
|
15
|
+
if (exact) {
|
|
16
|
+
return { ok: true, targetId: exact.targetId };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const lower = needle.toLowerCase();
|
|
20
|
+
const matches = tabs.map((t) => t.targetId).filter((id) => id.toLowerCase().startsWith(lower));
|
|
21
|
+
|
|
22
|
+
const only = matches.length === 1 ? matches[0] : undefined;
|
|
23
|
+
if (only) {
|
|
24
|
+
return { ok: true, targetId: only };
|
|
25
|
+
}
|
|
26
|
+
if (matches.length === 0) {
|
|
27
|
+
return { ok: false, reason: "not_found" };
|
|
28
|
+
}
|
|
29
|
+
return { ok: false, reason: "ambiguous", matches };
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { runExec } from "./enterprise-compat.js";
|
|
5
|
+
|
|
6
|
+
export async function movePathToTrash(targetPath: string): Promise<string> {
|
|
7
|
+
try {
|
|
8
|
+
await runExec("trash", [targetPath], { timeoutMs: 10_000 });
|
|
9
|
+
return targetPath;
|
|
10
|
+
} catch {
|
|
11
|
+
const trashDir = path.join(os.homedir(), ".Trash");
|
|
12
|
+
fs.mkdirSync(trashDir, { recursive: true });
|
|
13
|
+
const base = path.basename(targetPath);
|
|
14
|
+
let dest = path.join(trashDir, `${base}-${Date.now()}`);
|
|
15
|
+
if (fs.existsSync(dest)) {
|
|
16
|
+
dest = path.join(trashDir, `${base}-${Date.now()}-${Math.random()}`);
|
|
17
|
+
}
|
|
18
|
+
fs.renameSync(targetPath, dest);
|
|
19
|
+
return dest;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -3974,7 +3974,319 @@ function ToolsSection(props) {
|
|
|
3974
3974
|
})
|
|
3975
3975
|
),
|
|
3976
3976
|
|
|
3977
|
-
filtered.length === 0 && h('div', { style: { textAlign: 'center', padding: 40, color: 'var(--text-muted)' } }, 'No tools match this filter.')
|
|
3977
|
+
filtered.length === 0 && h('div', { style: { textAlign: 'center', padding: 40, color: 'var(--text-muted)' } }, 'No tools match this filter.'),
|
|
3978
|
+
|
|
3979
|
+
// ─── Browser Configuration ─────────────────────────
|
|
3980
|
+
h(BrowserConfigCard, { agentId: agentId }),
|
|
3981
|
+
|
|
3982
|
+
// ─── Tool Restrictions ─────────────────────────────
|
|
3983
|
+
h(ToolRestrictionsCard, { agentId: agentId })
|
|
3984
|
+
);
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
// ════════════════════════════════════════════════════════════
|
|
3988
|
+
// BROWSER CONFIG CARD — Configurable browser settings per agent
|
|
3989
|
+
// ════════════════════════════════════════════════════════════
|
|
3990
|
+
|
|
3991
|
+
function BrowserConfigCard(props) {
|
|
3992
|
+
var agentId = props.agentId;
|
|
3993
|
+
var _d = useApp(); var toast = _d.toast;
|
|
3994
|
+
var _cfg = useState(null); var cfg = _cfg[0]; var setCfg = _cfg[1];
|
|
3995
|
+
var _saving = useState(false); var saving = _saving[0]; var setSaving = _saving[1];
|
|
3996
|
+
var _collapsed = useState(true); var collapsed = _collapsed[0]; var setCollapsed = _collapsed[1];
|
|
3997
|
+
|
|
3998
|
+
function load() {
|
|
3999
|
+
engineCall('/bridge/agents/' + agentId + '/browser-config')
|
|
4000
|
+
.then(function(d) { setCfg(d.config || {}); })
|
|
4001
|
+
.catch(function() { setCfg({}); });
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
useEffect(function() { load(); }, [agentId]);
|
|
4005
|
+
|
|
4006
|
+
function save() {
|
|
4007
|
+
setSaving(true);
|
|
4008
|
+
engineCall('/bridge/agents/' + agentId + '/browser-config', {
|
|
4009
|
+
method: 'PUT',
|
|
4010
|
+
body: JSON.stringify(cfg),
|
|
4011
|
+
}).then(function() { toast('Browser config saved', 'success'); setSaving(false); })
|
|
4012
|
+
.catch(function(e) { toast(e.message, 'error'); setSaving(false); });
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
function update(key, value) {
|
|
4016
|
+
setCfg(function(prev) { var n = Object.assign({}, prev); n[key] = value; return n; });
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
if (!cfg) return null;
|
|
4020
|
+
|
|
4021
|
+
var labelStyle = { display: 'block', fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 4 };
|
|
4022
|
+
var helpStyle = { fontSize: 11, color: 'var(--text-muted)', marginTop: 2 };
|
|
4023
|
+
|
|
4024
|
+
return h('div', { className: 'card', style: { marginTop: 16 } },
|
|
4025
|
+
h('div', {
|
|
4026
|
+
className: 'card-header',
|
|
4027
|
+
style: { cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' },
|
|
4028
|
+
onClick: function() { setCollapsed(!collapsed); }
|
|
4029
|
+
},
|
|
4030
|
+
h('span', null, '\uD83C\uDF10 Browser Configuration'),
|
|
4031
|
+
h('span', { style: { fontSize: 12, color: 'var(--text-muted)' } }, collapsed ? '\u25BC' : '\u25B2')
|
|
4032
|
+
),
|
|
4033
|
+
!collapsed && h('div', { style: { padding: 16, display: 'grid', gap: 16 } },
|
|
4034
|
+
// Headless mode
|
|
4035
|
+
h('div', { className: 'form-group' },
|
|
4036
|
+
h('label', { style: labelStyle }, 'Headless Mode'),
|
|
4037
|
+
h('select', {
|
|
4038
|
+
className: 'input', value: cfg.headless !== false ? 'true' : 'false',
|
|
4039
|
+
onChange: function(e) { update('headless', e.target.value === 'true'); }
|
|
4040
|
+
},
|
|
4041
|
+
h('option', { value: 'true' }, 'Headless (no visible browser window)'),
|
|
4042
|
+
h('option', { value: 'false' }, 'Headed (visible browser — needed for Meet/Teams)')
|
|
4043
|
+
),
|
|
4044
|
+
h('div', { style: helpStyle }, 'Use headed mode for video calls (Google Meet, Teams) or visual debugging.')
|
|
4045
|
+
),
|
|
4046
|
+
|
|
4047
|
+
// SSRF Protection
|
|
4048
|
+
h('div', { className: 'form-group' },
|
|
4049
|
+
h('label', { style: labelStyle }, 'URL Protection'),
|
|
4050
|
+
h('select', {
|
|
4051
|
+
className: 'input', value: cfg.ssrfProtection || 'permissive',
|
|
4052
|
+
onChange: function(e) { update('ssrfProtection', e.target.value); }
|
|
4053
|
+
},
|
|
4054
|
+
h('option', { value: 'off' }, 'Off — No restrictions (full internet access)'),
|
|
4055
|
+
h('option', { value: 'permissive' }, 'Permissive — Block known dangerous URLs only'),
|
|
4056
|
+
h('option', { value: 'strict' }, 'Strict — Block private IPs, require allowlist')
|
|
4057
|
+
),
|
|
4058
|
+
h('div', { style: helpStyle }, 'Controls which URLs the agent can navigate to.')
|
|
4059
|
+
),
|
|
4060
|
+
|
|
4061
|
+
// Allow JS Evaluation
|
|
4062
|
+
h('div', { className: 'form-group' },
|
|
4063
|
+
h('label', { style: labelStyle }, 'JavaScript Evaluation'),
|
|
4064
|
+
h('select', {
|
|
4065
|
+
className: 'input', value: cfg.allowEvaluate !== false ? 'true' : 'false',
|
|
4066
|
+
onChange: function(e) { update('allowEvaluate', e.target.value === 'true'); }
|
|
4067
|
+
},
|
|
4068
|
+
h('option', { value: 'true' }, 'Allowed — Agent can run JS in page context'),
|
|
4069
|
+
h('option', { value: 'false' }, 'Blocked — No arbitrary JS execution')
|
|
4070
|
+
)
|
|
4071
|
+
),
|
|
4072
|
+
|
|
4073
|
+
// Allow File URLs
|
|
4074
|
+
h('div', { className: 'form-group' },
|
|
4075
|
+
h('label', { style: labelStyle }, 'File URLs (file://)'),
|
|
4076
|
+
h('select', {
|
|
4077
|
+
className: 'input', value: cfg.allowFileUrls ? 'true' : 'false',
|
|
4078
|
+
onChange: function(e) { update('allowFileUrls', e.target.value === 'true'); }
|
|
4079
|
+
},
|
|
4080
|
+
h('option', { value: 'false' }, 'Blocked — Cannot access local files'),
|
|
4081
|
+
h('option', { value: 'true' }, 'Allowed — Can open local file:// URLs')
|
|
4082
|
+
)
|
|
4083
|
+
),
|
|
4084
|
+
|
|
4085
|
+
// Max Contexts
|
|
4086
|
+
h('div', { className: 'form-group' },
|
|
4087
|
+
h('label', { style: labelStyle }, 'Max Browser Contexts'),
|
|
4088
|
+
h('input', {
|
|
4089
|
+
className: 'input', type: 'number', min: 1, max: 50,
|
|
4090
|
+
value: cfg.maxContexts || 10,
|
|
4091
|
+
onChange: function(e) { update('maxContexts', parseInt(e.target.value) || 10); }
|
|
4092
|
+
}),
|
|
4093
|
+
h('div', { style: helpStyle }, 'Maximum concurrent browser windows/tabs.')
|
|
4094
|
+
),
|
|
4095
|
+
|
|
4096
|
+
// Navigation Timeout
|
|
4097
|
+
h('div', { className: 'form-group' },
|
|
4098
|
+
h('label', { style: labelStyle }, 'Navigation Timeout (ms)'),
|
|
4099
|
+
h('input', {
|
|
4100
|
+
className: 'input', type: 'number', min: 5000, max: 120000, step: 1000,
|
|
4101
|
+
value: cfg.navigationTimeoutMs || 30000,
|
|
4102
|
+
onChange: function(e) { update('navigationTimeoutMs', parseInt(e.target.value) || 30000); }
|
|
4103
|
+
}),
|
|
4104
|
+
h('div', { style: helpStyle }, 'Maximum time to wait for page loads.')
|
|
4105
|
+
),
|
|
4106
|
+
|
|
4107
|
+
// Idle Timeout
|
|
4108
|
+
h('div', { className: 'form-group' },
|
|
4109
|
+
h('label', { style: labelStyle }, 'Idle Timeout (minutes)'),
|
|
4110
|
+
h('input', {
|
|
4111
|
+
className: 'input', type: 'number', min: 1, max: 60,
|
|
4112
|
+
value: Math.round((cfg.idleTimeoutMs || 300000) / 60000),
|
|
4113
|
+
onChange: function(e) { update('idleTimeoutMs', (parseInt(e.target.value) || 5) * 60000); }
|
|
4114
|
+
}),
|
|
4115
|
+
h('div', { style: helpStyle }, 'Close browser after this many minutes of inactivity.')
|
|
4116
|
+
),
|
|
4117
|
+
|
|
4118
|
+
// Blocked URL Patterns
|
|
4119
|
+
h('div', { className: 'form-group' },
|
|
4120
|
+
h('label', { style: labelStyle }, 'Blocked URL Patterns'),
|
|
4121
|
+
h('input', {
|
|
4122
|
+
className: 'input', placeholder: '*://169.254.*, *://metadata.google.*',
|
|
4123
|
+
value: (cfg.blockedUrlPatterns || []).join(', '),
|
|
4124
|
+
onChange: function(e) { update('blockedUrlPatterns', e.target.value.split(',').map(function(s) { return s.trim(); }).filter(Boolean)); }
|
|
4125
|
+
}),
|
|
4126
|
+
h('div', { style: helpStyle }, 'Comma-separated URL patterns to block (supports *).')
|
|
4127
|
+
),
|
|
4128
|
+
|
|
4129
|
+
// Allowed URL Patterns (only for strict mode)
|
|
4130
|
+
cfg.ssrfProtection === 'strict' && h('div', { className: 'form-group' },
|
|
4131
|
+
h('label', { style: labelStyle }, 'Allowed URL Patterns (Strict Mode)'),
|
|
4132
|
+
h('input', {
|
|
4133
|
+
className: 'input', placeholder: '*://example.com/*, *://api.service.com/*',
|
|
4134
|
+
value: (cfg.allowedUrlPatterns || []).join(', '),
|
|
4135
|
+
onChange: function(e) { update('allowedUrlPatterns', e.target.value.split(',').map(function(s) { return s.trim(); }).filter(Boolean)); }
|
|
4136
|
+
}),
|
|
4137
|
+
h('div', { style: helpStyle }, 'Only these URLs are allowed in strict mode.')
|
|
4138
|
+
),
|
|
4139
|
+
|
|
4140
|
+
// Save button
|
|
4141
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end', paddingTop: 8 } },
|
|
4142
|
+
h('button', { className: 'btn', disabled: saving, onClick: save }, saving ? 'Saving...' : 'Save Browser Config')
|
|
4143
|
+
)
|
|
4144
|
+
)
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
// ════════════════════════════════════════════════════════════
|
|
4149
|
+
// TOOL RESTRICTIONS CARD — Per-agent restrictions
|
|
4150
|
+
// ════════════════════════════════════════════════════════════
|
|
4151
|
+
|
|
4152
|
+
function ToolRestrictionsCard(props) {
|
|
4153
|
+
var agentId = props.agentId;
|
|
4154
|
+
var _d = useApp(); var toast = _d.toast;
|
|
4155
|
+
var _cfg = useState(null); var cfg = _cfg[0]; var setCfg = _cfg[1];
|
|
4156
|
+
var _saving = useState(false); var saving = _saving[0]; var setSaving = _saving[1];
|
|
4157
|
+
var _collapsed = useState(true); var collapsed = _collapsed[0]; var setCollapsed = _collapsed[1];
|
|
4158
|
+
|
|
4159
|
+
function load() {
|
|
4160
|
+
engineCall('/bridge/agents/' + agentId + '/tool-restrictions')
|
|
4161
|
+
.then(function(d) { setCfg(d.restrictions || {}); })
|
|
4162
|
+
.catch(function() { setCfg({}); });
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
useEffect(function() { load(); }, [agentId]);
|
|
4166
|
+
|
|
4167
|
+
function save() {
|
|
4168
|
+
setSaving(true);
|
|
4169
|
+
engineCall('/bridge/agents/' + agentId + '/tool-restrictions', {
|
|
4170
|
+
method: 'PUT',
|
|
4171
|
+
body: JSON.stringify(cfg),
|
|
4172
|
+
}).then(function() { toast('Restrictions saved', 'success'); setSaving(false); })
|
|
4173
|
+
.catch(function(e) { toast(e.message, 'error'); setSaving(false); });
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
function update(key, value) {
|
|
4177
|
+
setCfg(function(prev) { var n = Object.assign({}, prev); n[key] = value; return n; });
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
if (!cfg) return null;
|
|
4181
|
+
|
|
4182
|
+
var labelStyle = { display: 'block', fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 4 };
|
|
4183
|
+
var helpStyle = { fontSize: 11, color: 'var(--text-muted)', marginTop: 2 };
|
|
4184
|
+
|
|
4185
|
+
return h('div', { className: 'card', style: { marginTop: 16 } },
|
|
4186
|
+
h('div', {
|
|
4187
|
+
className: 'card-header',
|
|
4188
|
+
style: { cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between' },
|
|
4189
|
+
onClick: function() { setCollapsed(!collapsed); }
|
|
4190
|
+
},
|
|
4191
|
+
h('span', null, '\uD83D\uDD12 Tool Restrictions'),
|
|
4192
|
+
h('span', { style: { fontSize: 12, color: 'var(--text-muted)' } }, collapsed ? '\u25BC' : '\u25B2')
|
|
4193
|
+
),
|
|
4194
|
+
!collapsed && h('div', { style: { padding: 16, display: 'grid', gap: 16 } },
|
|
4195
|
+
// Max file size for read/write
|
|
4196
|
+
h('div', { className: 'form-group' },
|
|
4197
|
+
h('label', { style: labelStyle }, 'Max File Size (MB)'),
|
|
4198
|
+
h('input', {
|
|
4199
|
+
className: 'input', type: 'number', min: 1, max: 1000,
|
|
4200
|
+
value: cfg.maxFileSizeMb || 50,
|
|
4201
|
+
onChange: function(e) { update('maxFileSizeMb', parseInt(e.target.value) || 50); }
|
|
4202
|
+
}),
|
|
4203
|
+
h('div', { style: helpStyle }, 'Maximum file size the agent can read or write.')
|
|
4204
|
+
),
|
|
4205
|
+
|
|
4206
|
+
// Shell command execution
|
|
4207
|
+
h('div', { className: 'form-group' },
|
|
4208
|
+
h('label', { style: labelStyle }, 'Shell Command Execution'),
|
|
4209
|
+
h('select', {
|
|
4210
|
+
className: 'input', value: cfg.shellExecution || 'allowed',
|
|
4211
|
+
onChange: function(e) { update('shellExecution', e.target.value); }
|
|
4212
|
+
},
|
|
4213
|
+
h('option', { value: 'allowed' }, 'Allowed — Full shell access'),
|
|
4214
|
+
h('option', { value: 'sandboxed' }, 'Sandboxed — Limited to safe commands'),
|
|
4215
|
+
h('option', { value: 'blocked' }, 'Blocked — No shell execution')
|
|
4216
|
+
),
|
|
4217
|
+
h('div', { style: helpStyle }, 'Controls whether the agent can run shell commands.')
|
|
4218
|
+
),
|
|
4219
|
+
|
|
4220
|
+
// Web fetch restrictions
|
|
4221
|
+
h('div', { className: 'form-group' },
|
|
4222
|
+
h('label', { style: labelStyle }, 'Web Fetch'),
|
|
4223
|
+
h('select', {
|
|
4224
|
+
className: 'input', value: cfg.webFetch || 'allowed',
|
|
4225
|
+
onChange: function(e) { update('webFetch', e.target.value); }
|
|
4226
|
+
},
|
|
4227
|
+
h('option', { value: 'allowed' }, 'Allowed — Can fetch any URL'),
|
|
4228
|
+
h('option', { value: 'restricted' }, 'Restricted — Only allowed domains'),
|
|
4229
|
+
h('option', { value: 'blocked' }, 'Blocked — No web fetching')
|
|
4230
|
+
)
|
|
4231
|
+
),
|
|
4232
|
+
|
|
4233
|
+
// Email sending restrictions
|
|
4234
|
+
h('div', { className: 'form-group' },
|
|
4235
|
+
h('label', { style: labelStyle }, 'Email Sending'),
|
|
4236
|
+
h('select', {
|
|
4237
|
+
className: 'input', value: cfg.emailSending || 'allowed',
|
|
4238
|
+
onChange: function(e) { update('emailSending', e.target.value); }
|
|
4239
|
+
},
|
|
4240
|
+
h('option', { value: 'allowed' }, 'Allowed — Can send to anyone'),
|
|
4241
|
+
h('option', { value: 'internal' }, 'Internal Only — Same domain only'),
|
|
4242
|
+
h('option', { value: 'approval' }, 'Requires Approval — Manager must approve'),
|
|
4243
|
+
h('option', { value: 'blocked' }, 'Blocked — No email sending')
|
|
4244
|
+
),
|
|
4245
|
+
h('div', { style: helpStyle }, 'Controls who the agent can email.')
|
|
4246
|
+
),
|
|
4247
|
+
|
|
4248
|
+
// Database access
|
|
4249
|
+
h('div', { className: 'form-group' },
|
|
4250
|
+
h('label', { style: labelStyle }, 'Database Access'),
|
|
4251
|
+
h('select', {
|
|
4252
|
+
className: 'input', value: cfg.databaseAccess || 'readwrite',
|
|
4253
|
+
onChange: function(e) { update('databaseAccess', e.target.value); }
|
|
4254
|
+
},
|
|
4255
|
+
h('option', { value: 'readwrite' }, 'Read + Write — Full database access'),
|
|
4256
|
+
h('option', { value: 'readonly' }, 'Read Only — SELECT queries only'),
|
|
4257
|
+
h('option', { value: 'blocked' }, 'Blocked — No database access')
|
|
4258
|
+
)
|
|
4259
|
+
),
|
|
4260
|
+
|
|
4261
|
+
// Drive/file sharing
|
|
4262
|
+
h('div', { className: 'form-group' },
|
|
4263
|
+
h('label', { style: labelStyle }, 'File Sharing (Drive)'),
|
|
4264
|
+
h('select', {
|
|
4265
|
+
className: 'input', value: cfg.fileSharing || 'allowed',
|
|
4266
|
+
onChange: function(e) { update('fileSharing', e.target.value); }
|
|
4267
|
+
},
|
|
4268
|
+
h('option', { value: 'allowed' }, 'Allowed — Can share files externally'),
|
|
4269
|
+
h('option', { value: 'internal' }, 'Internal Only — Share within org only'),
|
|
4270
|
+
h('option', { value: 'blocked' }, 'Blocked — No file sharing')
|
|
4271
|
+
)
|
|
4272
|
+
),
|
|
4273
|
+
|
|
4274
|
+
// Rate limiting
|
|
4275
|
+
h('div', { className: 'form-group' },
|
|
4276
|
+
h('label', { style: labelStyle }, 'Rate Limit (calls per minute)'),
|
|
4277
|
+
h('input', {
|
|
4278
|
+
className: 'input', type: 'number', min: 0, max: 1000,
|
|
4279
|
+
value: cfg.rateLimit || 0,
|
|
4280
|
+
onChange: function(e) { update('rateLimit', parseInt(e.target.value) || 0); }
|
|
4281
|
+
}),
|
|
4282
|
+
h('div', { style: helpStyle }, '0 = no limit. Applies across all tool calls.')
|
|
4283
|
+
),
|
|
4284
|
+
|
|
4285
|
+
// Save button
|
|
4286
|
+
h('div', { style: { display: 'flex', justifyContent: 'flex-end', paddingTop: 8 } },
|
|
4287
|
+
h('button', { className: 'btn', disabled: saving, onClick: save }, saving ? 'Saving...' : 'Save Restrictions')
|
|
4288
|
+
)
|
|
4289
|
+
)
|
|
3978
4290
|
);
|
|
3979
4291
|
}
|
|
3980
4292
|
|