@xopcai/xopc 0.0.264 → 0.0.265
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/browser-ext/dist/assets/auth-CknJ00ae.js +5844 -0
- package/dist/browser-ext/dist/assets/sidepanel-C69P3v9u.css +113 -0
- package/dist/browser-ext/dist/background.js +200 -68
- package/dist/browser-ext/dist/sidepanel.html +14 -0
- package/dist/browser-ext/dist/sidepanel.js +18832 -0
- package/dist/browser-ext/manifest.json +20 -20
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{activity-store-BYBCpt7_.js → activity-store-Bhyy6ENz.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-D20aBHPJ.js → agent-avatar-dicebear-DRTTU2H3.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-defaults-settings-panel-BZvMqNrU.js → agent-defaults-settings-panel-BsXF7LVi.js} +1 -1
- package/dist/gateway/static/root/assets/{agents-6foBELwt.js → agents-DpjWfxYx.js} +1 -1
- package/dist/gateway/static/root/assets/{api-YbtjDbuG.js → api-B2y7ZjZk.js} +1 -1
- package/dist/gateway/static/root/assets/{app-management-settings-panel-4ps1RrDU.js → app-management-settings-panel-33Mc81h2.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-C7nBHnkG.js → appearance-settings-C8JECSiJ.js} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-B_g-rL5b.js → apps-page-t64FcxIZ.js} +1 -1
- package/dist/gateway/static/root/assets/{archive-plugin-DfKK1iJO.js → archive-plugin-PzZ6SzfY.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-BOU1pdnQ.js → attachment-load-OhLL6K__.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-page-BePzexAH.js → automations-page-BpAYwuvx.js} +1 -1
- package/dist/gateway/static/root/assets/{automations-workspace-7-m5pBEZ.js → automations-workspace-B6aTT5IC.js} +1 -1
- package/dist/gateway/static/root/assets/{binary-plugins-Cly8XDyt.js → binary-plugins-B5XpGsHg.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-DzSu62js.js → block-editor-BgEhoTnN.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-automation-inputs-8bHj7Fv2.js → browser-automation-inputs-s5I78Iko.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-automations-page-CzL7Aj2k.js → browser-automations-page-CBLunDRH.js} +1 -1
- package/dist/gateway/static/root/assets/{browser-settings-page-Dum3mTiz.js → browser-settings-page-CSfpPCuD.js} +1 -1
- package/dist/gateway/static/root/assets/{capabilities-settings-panel-Ci6qVS-O.js → capabilities-settings-panel-DftNnICK.js} +2 -2
- package/dist/gateway/static/root/assets/{channels-settings-BOGEZnSv.js → channels-settings-CpuWa5qu.js} +1 -1
- package/dist/gateway/static/root/assets/{channels-status-swr-BR8oQ_eP.js → channels-status-swr-_OsWr_vS.js} +1 -1
- package/dist/gateway/static/root/assets/{chat-terminal-dock-BF9ZXa5Z.js → chat-terminal-dock-mnPsS_GJ.js} +1 -1
- package/dist/gateway/static/root/assets/{connectors-page-DBvEvupa.js → connectors-page-D3nHGnNV.js} +1 -1
- package/dist/gateway/static/root/assets/{cron-expression-BszT_gjK.js → cron-expression-NcGBvIfe.js} +1 -1
- package/dist/gateway/static/root/assets/{date-picker-D2MKBJ0G.js → date-picker-BrVcl7H5.js} +1 -1
- package/dist/gateway/static/root/assets/{dependency-picker-DqigBx-G.js → dependency-picker--edd48Y9.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-settings-DMPk4CaK.js → desktop-pet-settings-DWredJwO.js} +1 -1
- package/dist/gateway/static/root/assets/{desktop-pet-CapspBGG.js → desktop-pet-vuyyb6I6.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-CXLsDtrW.js → extension-debug-page-CtBW4ebj.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-B2j87ROw.js → extension-page-CsUrmtW7.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-t1BApw-u.js → extension-settings-page-BUd31AuK.js} +1 -1
- package/dist/gateway/static/root/assets/{files-BArWTfr7.js → files-Be5viEFY.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-swr-BFSesvAC.js → gateway-config-swr-OObCK8jM.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-settings-BFAecoGy.js → gateway-settings-C6FQv7op.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-startup-retry-D1giYgI-.js → gateway-startup-retry-D_TxKN_A.js} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-settings-CyD9zDcU.js → heartbeat-settings-CDo1Gjp2.js} +1 -1
- package/dist/gateway/static/root/assets/{home-page-uItZMiBE.js → home-page-D3_uvVJY.js} +1 -1
- package/dist/gateway/static/root/assets/{image-generation-api-BeB-OPqO.js → image-generation-api-fPU9yGsw.js} +1 -1
- package/dist/gateway/static/root/assets/index-Dpu7EKTm.js +74 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-Q_CM-ZCs.js → keyboard-shortcuts-settings-CNVmrCrV.js} +1 -1
- package/dist/gateway/static/root/assets/{local-app-workbench-page-DaiBWfOt.js → local-app-workbench-page-B0IVDvs-.js} +1 -1
- package/dist/gateway/static/root/assets/{local-apps-page-BALMNCSp.js → local-apps-page-DvkarvLt.js} +1 -1
- package/dist/gateway/static/root/assets/{locale-store-RJHYFoq9.js → locale-store-DFQX9kTl.js} +1 -1
- package/dist/gateway/static/root/assets/{logs-page-C-TS7oFg.js → logs-page-BknY0vZf.js} +1 -1
- package/dist/gateway/static/root/assets/{management-settings-BajkJeYl.js → management-settings-DW6hJINQ.js} +1 -1
- package/dist/gateway/static/root/assets/{markdown-view-BxoQJqHx.js → markdown-view-D-5jHRWg.js} +1 -1
- package/dist/gateway/static/root/assets/{media-plugins-C_8Hl6KG.js → media-plugins-DYautOWY.js} +1 -1
- package/dist/gateway/static/root/assets/{messages-Cf1h5WXB.js → messages-Dj8QuS6e.js} +2 -2
- package/dist/gateway/static/root/assets/{mobile-pairing-wizard-CsWIZrZW.js → mobile-pairing-wizard-Dgu03cv-.js} +1 -1
- package/dist/gateway/static/root/assets/{note-detail-panel-CCbPg_GP.js → note-detail-panel-Cv9uH6Jq.js} +3 -3
- package/dist/gateway/static/root/assets/{notes-page-BdxxcUKa.js → notes-page-DcIAiYJn.js} +1 -1
- package/dist/gateway/static/root/assets/{page-tabs-BMYQef5D.js → page-tabs-DF6Q5XvA.js} +1 -1
- package/dist/gateway/static/root/assets/{preview-open-alternatives-CKZ-H5hJ.js → preview-open-alternatives-CQJH_jAN.js} +1 -1
- package/dist/gateway/static/root/assets/{product-delivery-CV4pDOtG.js → product-delivery-CCC4FJ3Y.js} +1 -1
- package/dist/gateway/static/root/assets/{product-open-page-zXJIptXs.js → product-open-page-CIGLPFm7.js} +1 -1
- package/dist/gateway/static/root/assets/{project-detail-page-DRhhxhZB.js → project-detail-page-D65JfNSw.js} +1 -1
- package/dist/gateway/static/root/assets/{projects-page-Wt7gF3Sx.js → projects-page-BfWF7Sz-.js} +1 -1
- package/dist/gateway/static/root/assets/{registry-api-BSfFaxVN.js → registry-api-CsZLatKo.js} +1 -1
- package/dist/gateway/static/root/assets/{remote-access-hub-B6de5Zrb.js → remote-access-hub-DA1g4CUD.js} +1 -1
- package/dist/gateway/static/root/assets/{runtime-tools-settings-panel-DcUB5h1f.js → runtime-tools-settings-panel-0-bFSBB8.js} +1 -1
- package/dist/gateway/static/root/assets/{schema-form-CquWhe7r.js → schema-form-csGbhTUr.js} +1 -1
- package/dist/gateway/static/root/assets/{schemas-BHS7bo06.js → schemas-BaskVM5g.js} +2 -2
- package/dist/gateway/static/root/assets/{sessions-page-BNWO2EVF.js → sessions-page-DXPBNhrZ.js} +1 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-DxeZm67O.js +1 -0
- package/dist/gateway/static/root/assets/{settings-form-section-DwswAefc.js → settings-form-section-YdrdY8B3.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-CoAVmjjs.js → settings-page-Cv9waRn3.js} +1 -1
- package/dist/gateway/static/root/assets/{setup-status-panel-C9EIcWtH.js → setup-status-panel-DPf-_05Z.js} +1 -1
- package/dist/gateway/static/root/assets/{share-preview-page-DWwy_4zh.js → share-preview-page-DBoEHTPn.js} +1 -1
- package/dist/gateway/static/root/assets/{shares-settings-CIUvq27-.js → shares-settings-BQP5yNcO.js} +1 -1
- package/dist/gateway/static/root/assets/skill-reload-api-B5T8cgjq.js +1 -0
- package/dist/gateway/static/root/assets/{skills-page-CqsgVtVh.js → skills-page-D1G8h2Lt.js} +1 -1
- package/dist/gateway/static/root/assets/{skills-page.utils-B-N73248.js → skills-page.utils-BNZYTop7.js} +1 -1
- package/dist/gateway/static/root/assets/{system-settings-panel-BndjXuBZ.js → system-settings-panel-ByuppbHi.js} +1 -1
- package/dist/gateway/static/root/assets/{task-detail-page-CG6Ha9t7.js → task-detail-page-DNmkrF6y.js} +1 -1
- package/dist/gateway/static/root/assets/{theme-store-C5ssn64t.js → theme-store-zRT6vaZc.js} +1 -1
- package/dist/gateway/static/root/assets/{tunnel-api-BxZdcp3h.js → tunnel-api-CxWGpbwr.js} +1 -1
- package/dist/gateway/static/root/assets/{use-autosave-mj9CUuz1.js → use-autosave-B0l4qubU.js} +1 -1
- package/dist/gateway/static/root/assets/{user-model-page-DKeHMXI4.js → user-model-page-DMsI6YI7.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-api-key-field-CzxVMYlr.js → voice-api-key-field-Bcz7boJg.js} +1 -1
- package/dist/gateway/static/root/assets/{voice-config-api-COA0V1cK.js → voice-config-api-DpJEu2RQ.js} +1 -1
- package/dist/gateway/static/root/assets/{work-discovery-overlay-DMiPK8dp.js → work-discovery-overlay-Dujq1j_u.js} +1 -1
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-C82OXCJK.js → workflow-run-setup-panel-CxNYbLNB.js} +1 -1
- package/dist/gateway/static/root/assets/{workflows-page-HLCUV3rQ.js → workflows-page-Dk1jK2nv.js} +1 -1
- package/dist/gateway/static/root/index.html +12 -12
- package/dist/package.js +1 -1
- package/dist/packages/browser-control-contract/src/index.js +14 -0
- package/dist/packages/endpoint-tools-protocol/src/index.js +2 -1
- package/dist/packages/gateway-contract/src/browser-page-context.js +43 -0
- package/dist/packages/gateway-contract/src/browser-tab-binding.js +29 -0
- package/dist/packages/gateway-contract/src/device-pairing.js +18 -2
- package/dist/packages/realtime-protocol/src/index.js +1 -0
- package/dist/packages/realtime-protocol/src/voice-audio.js +70 -9
- package/dist/packages/realtime-protocol/src/voice.js +65 -17
- package/dist/src/agent/source-context/browser-page.d.ts +3 -0
- package/dist/src/agent/source-context/browser-page.js +39 -0
- package/dist/src/agent/source-context/injector.js +9 -2
- package/dist/src/agent/source-context/types.d.ts +7 -1
- package/dist/src/agent/source-context/types.js +4 -1
- package/dist/src/agent/tools/factory.js +9 -1
- package/dist/src/browser/automations/schema.d.ts +1 -1
- package/dist/src/browser/automations/service.d.ts +1 -1
- package/dist/src/browser/native-messaging-host.d.ts +15 -0
- package/dist/src/browser/native-messaging-host.js +105 -0
- package/dist/src/browser/providers/browser-ext-install.d.ts +17 -1
- package/dist/src/browser/providers/browser-ext-install.js +82 -16
- package/dist/src/browser/providers/extension.d.ts +4 -0
- package/dist/src/browser/providers/extension.js +104 -38
- package/dist/src/browser/runtime/browser-runtime.d.ts +2 -1
- package/dist/src/browser/runtime/browser-runtime.js +6 -2
- package/dist/src/cli/commands/browser.js +13 -3
- package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
- package/dist/src/endpoint-tools/runtime.js +2 -0
- package/dist/src/gateway/chat-stream/mapper.js +3 -2
- package/dist/src/gateway/hono/app.js +19 -3
- package/dist/src/gateway/hono/middleware/auth.d.ts +4 -1
- package/dist/src/gateway/hono/middleware/auth.js +12 -1
- package/dist/src/gateway/hono/routes/browser-install.js +1 -1
- package/dist/src/gateway/hono/routes/browser.js +106 -7
- package/dist/src/gateway/hono/routes/devices.js +36 -3
- package/dist/src/gateway/hono/routes/endpoint-tools.js +15 -0
- package/dist/src/gateway/hono/routes/session-input-handler.d.ts +20 -2
- package/dist/src/gateway/hono/routes/session-input-handler.js +18 -0
- package/dist/src/gateway/hono/routes/sessions.js +12 -6
- package/dist/src/gateway/hono/routes/voice.js +5 -4
- package/dist/src/gateway/security/gateway-scopes.d.ts +1 -0
- package/dist/src/gateway/security/gateway-scopes.js +10 -1
- package/dist/src/gateway/security/origin-check.d.ts +1 -0
- package/dist/src/gateway/security/origin-check.js +5 -1
- package/dist/src/gateway/service/agent-runner.js +2 -1
- package/dist/src/gateway/service/session-input-coordinator.d.ts +1 -0
- package/dist/src/gateway/service/session-input-coordinator.js +5 -4
- package/dist/src/gateway/service.js +12 -11
- package/dist/src/realtime/runtime.js +2 -1
- package/dist/src/session/client-history.d.ts +4 -1
- package/dist/src/session/client-history.js +6 -3
- package/dist/src/storage/sqlite/browser-tab-binding-repository.d.ts +7 -0
- package/dist/src/storage/sqlite/browser-tab-binding-repository.js +60 -0
- package/dist/src/storage/sqlite/device-access-repository.d.ts +3 -1
- package/dist/src/storage/sqlite/device-access-repository.js +4 -3
- package/dist/src/storage/sqlite/device-pairing-approval.js +6 -4
- package/dist/src/storage/sqlite/device-pairing-repository.d.ts +1 -1
- package/dist/src/storage/sqlite/index.d.ts +1 -0
- package/dist/src/storage/sqlite/index.js +2 -1
- package/dist/src/storage/sqlite/migrations/155_browser_extension.sql +120 -0
- package/dist/src/storage/sqlite/migrations/156_browser_tab_bindings.sql +14 -0
- package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
- package/dist/src/storage/sqlite/migrations/runner.js +3 -3
- package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
- package/dist/src/storage/sqlite/schema.js +1 -1
- package/dist/src/voice/realtime/agentBroker.d.ts +54 -0
- package/dist/src/voice/realtime/agentBroker.js +116 -0
- package/dist/src/voice/realtime/agentEngine.js +62 -15
- package/dist/src/voice/realtime/engine.d.ts +2 -1
- package/dist/src/voice/realtime/omniEngine.js +36 -7
- package/dist/src/voice/realtime/pcmFrameBuffer.d.ts +6 -0
- package/dist/src/voice/realtime/pcmFrameBuffer.js +29 -0
- package/dist/src/voice/realtime/runtime.js +97 -21
- package/dist/src/voice/realtime/runtime.types.d.ts +5 -21
- package/dist/src/voice/realtime/turnPolicy.d.ts +36 -0
- package/dist/src/voice/realtime/turnPolicy.js +134 -0
- package/package.json +2 -2
- package/dist/browser-ext/dist/content.js +0 -191
- package/dist/browser-ext/dist/popup.js +0 -32
- package/dist/browser-ext/popup.html +0 -37
- package/dist/gateway/static/root/assets/index-DQtjs2x3.js +0 -74
- package/dist/gateway/static/root/assets/settings-advanced-gate-DK1yTYE4.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-DfE3zFVw.js +0 -1
- package/dist/src/voice/realtime/conversationTurn.d.ts +0 -20
- package/dist/src/voice/realtime/conversationTurn.js +0 -83
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Fixed path — gateway upgrades overwrite in place so Chrome sideload paths stay stable.
|
|
4
4
|
*/
|
|
5
5
|
import type { Config } from '../../config/schema.js';
|
|
6
|
-
export declare const BROWSER_EXT_REQUIRED_FILES: readonly ["manifest.json", "
|
|
6
|
+
export declare const BROWSER_EXT_REQUIRED_FILES: readonly ["manifest.json", "dist/background.js", "dist/sidepanel.html"];
|
|
7
7
|
export type BrowserExtBundledFrom = 'npm-dist' | 'git-dev' | 'electron-asar' | 'env-override';
|
|
8
8
|
export interface BrowserExtInstallMeta {
|
|
9
9
|
xopcVersion: string;
|
|
@@ -32,6 +32,11 @@ export interface EnsureBrowserExtResult {
|
|
|
32
32
|
xopcVersion: string;
|
|
33
33
|
copied: boolean;
|
|
34
34
|
}
|
|
35
|
+
export interface BrowserNativeHostInstallResult {
|
|
36
|
+
installed: boolean;
|
|
37
|
+
manifestPaths: string[];
|
|
38
|
+
reason?: string;
|
|
39
|
+
}
|
|
35
40
|
/** Validate a directory contains a loadable extension tree. */
|
|
36
41
|
export declare function validateBrowserExtLayout(dir: string): boolean;
|
|
37
42
|
export declare function browserExtContentHash(dir: string): string;
|
|
@@ -59,6 +64,17 @@ export declare function ensureBrowserExtensionArtifacts(opts?: {
|
|
|
59
64
|
force?: boolean;
|
|
60
65
|
cacheDir?: string;
|
|
61
66
|
}): Promise<EnsureBrowserExtResult>;
|
|
67
|
+
export declare function browserNativeManifestDirectories(platform?: NodeJS.Platform, home?: string): string[];
|
|
68
|
+
/** Install the local-only bootstrap host used to discover and pair with a Gateway. */
|
|
69
|
+
export declare function installBrowserNativeMessagingHost(opts?: {
|
|
70
|
+
cacheDir?: string;
|
|
71
|
+
cliPath?: string;
|
|
72
|
+
nodePath?: string;
|
|
73
|
+
platform?: NodeJS.Platform;
|
|
74
|
+
home?: string;
|
|
75
|
+
configPath?: string;
|
|
76
|
+
stateDir?: string;
|
|
77
|
+
}): Promise<BrowserNativeHostInstallResult>;
|
|
62
78
|
/** Gateway startup hook: ensure artifacts only while the extension driver is active. */
|
|
63
79
|
export declare function ensureBrowserExtensionOnStartup(config: Config | undefined): Promise<void>;
|
|
64
80
|
export declare function readInstalledExtensionDir(cacheDir?: string): Promise<string | null>;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { PACKAGE_VERSION, init_package_version } from "../../package-version.js";
|
|
2
|
+
import { resolveStateDir } from "../../config/paths-state.js";
|
|
2
3
|
import { createLogger } from "../../utils/logger/index.js";
|
|
3
4
|
import { init_logger } from "../../utils/logger.js";
|
|
4
|
-
import { init_paths, resolveBinDir } from "../../config/paths.js";
|
|
5
|
+
import { init_paths, resolveBinDir, resolveConfigPath } from "../../config/paths.js";
|
|
5
6
|
import { init_write_file_atomic, writeTextAtomic } from "../../infra/write-file-atomic.js";
|
|
6
|
-
import { resolvePackageRoot } from "../../infra/update-check.js";
|
|
7
7
|
import { assertCacheDir } from "../cache-dir-policy.js";
|
|
8
|
+
import { BROWSER_EXTENSION_ID, BROWSER_NATIVE_HOST_NAME } from "../native-messaging-host.js";
|
|
9
|
+
import { resolvePackageRoot } from "../../infra/update-check.js";
|
|
8
10
|
import { dirname, join } from "node:path";
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
10
12
|
import { fileURLToPath } from "node:url";
|
|
11
13
|
import { spawn } from "node:child_process";
|
|
12
14
|
import { createHash } from "node:crypto";
|
|
@@ -25,16 +27,13 @@ const META_FILENAME = ".meta.json";
|
|
|
25
27
|
const STAGING_MAX_AGE_MS = 3600 * 1e3;
|
|
26
28
|
const INSTALLED_ARTIFACT_NAMES = [
|
|
27
29
|
"manifest.json",
|
|
28
|
-
"popup.html",
|
|
29
30
|
"dist",
|
|
30
31
|
"icons"
|
|
31
32
|
];
|
|
32
33
|
const BROWSER_EXT_REQUIRED_FILES = [
|
|
33
34
|
"manifest.json",
|
|
34
|
-
"popup.html",
|
|
35
35
|
"dist/background.js",
|
|
36
|
-
"dist/
|
|
37
|
-
"dist/popup.js"
|
|
36
|
+
"dist/sidepanel.html"
|
|
38
37
|
];
|
|
39
38
|
function moduleDir() {
|
|
40
39
|
return dirname(fileURLToPath(import.meta.url));
|
|
@@ -45,7 +44,16 @@ function validateBrowserExtLayout(dir) {
|
|
|
45
44
|
}
|
|
46
45
|
function browserExtContentHash(dir) {
|
|
47
46
|
const hash = createHash("sha256");
|
|
48
|
-
|
|
47
|
+
const collectFiles = (relativeDir) => readdirSync(join(dir, relativeDir), { withFileTypes: true }).flatMap((entry) => {
|
|
48
|
+
const relativePath = join(relativeDir, entry.name);
|
|
49
|
+
return entry.isDirectory() ? collectFiles(relativePath) : [relativePath];
|
|
50
|
+
});
|
|
51
|
+
const relativePaths = [
|
|
52
|
+
"manifest.json",
|
|
53
|
+
...collectFiles("dist"),
|
|
54
|
+
...collectFiles("icons")
|
|
55
|
+
].sort();
|
|
56
|
+
for (const relativePath of relativePaths) {
|
|
49
57
|
hash.update(relativePath);
|
|
50
58
|
hash.update(readFileSync(join(dir, relativePath)));
|
|
51
59
|
}
|
|
@@ -177,21 +185,25 @@ function copyBundledFile(src, dest) {
|
|
|
177
185
|
mkdirSync(dirname(dest), { recursive: true });
|
|
178
186
|
writeFileSync(dest, readFileSync(src));
|
|
179
187
|
}
|
|
180
|
-
const BROWSER_EXT_DIST_FILES = [
|
|
181
|
-
"background.js",
|
|
182
|
-
"content.js",
|
|
183
|
-
"popup.js"
|
|
184
|
-
];
|
|
185
188
|
const BROWSER_EXT_ICON_FILES = [
|
|
186
189
|
"icon-16.png",
|
|
187
190
|
"icon-32.png",
|
|
188
191
|
"icon-48.png",
|
|
189
192
|
"icon-128.png"
|
|
190
193
|
];
|
|
194
|
+
function copyBundledDirectory(src, dest) {
|
|
195
|
+
mkdirSync(dest, { recursive: true });
|
|
196
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
197
|
+
const sourcePath = join(src, entry.name);
|
|
198
|
+
const destinationPath = join(dest, entry.name);
|
|
199
|
+
if (entry.isDirectory()) copyBundledDirectory(sourcePath, destinationPath);
|
|
200
|
+
else copyBundledFile(sourcePath, destinationPath);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
191
203
|
function copyBundledTree(src, dest) {
|
|
192
204
|
mkdirSync(dest, { recursive: true });
|
|
193
|
-
|
|
194
|
-
|
|
205
|
+
copyBundledFile(join(src, "manifest.json"), join(dest, "manifest.json"));
|
|
206
|
+
copyBundledDirectory(join(src, "dist"), join(dest, "dist"));
|
|
195
207
|
for (const icon of BROWSER_EXT_ICON_FILES) {
|
|
196
208
|
const iconSrc = join(src, "icons", icon);
|
|
197
209
|
if (existsSync(iconSrc)) copyBundledFile(iconSrc, join(dest, "icons", icon));
|
|
@@ -283,6 +295,60 @@ async function ensureBrowserExtensionArtifacts(opts) {
|
|
|
283
295
|
copied: true
|
|
284
296
|
};
|
|
285
297
|
}
|
|
298
|
+
function shellQuote(value) {
|
|
299
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
300
|
+
}
|
|
301
|
+
function browserNativeManifestDirectories(platform = process.platform, home = process.env.HOME || process.env.USERPROFILE || "") {
|
|
302
|
+
if (!home) return [];
|
|
303
|
+
if (platform === "darwin") return [
|
|
304
|
+
join(home, "Library/Application Support/Google/Chrome/NativeMessagingHosts"),
|
|
305
|
+
join(home, "Library/Application Support/Chromium/NativeMessagingHosts"),
|
|
306
|
+
join(home, "Library/Application Support/Microsoft Edge/NativeMessagingHosts"),
|
|
307
|
+
join(home, "Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts")
|
|
308
|
+
];
|
|
309
|
+
if (platform === "linux") return [
|
|
310
|
+
join(home, ".config/google-chrome/NativeMessagingHosts"),
|
|
311
|
+
join(home, ".config/chromium/NativeMessagingHosts"),
|
|
312
|
+
join(home, ".config/microsoft-edge/NativeMessagingHosts"),
|
|
313
|
+
join(home, ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts")
|
|
314
|
+
];
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
/** Install the local-only bootstrap host used to discover and pair with a Gateway. */
|
|
318
|
+
async function installBrowserNativeMessagingHost(opts) {
|
|
319
|
+
const directories = browserNativeManifestDirectories(opts?.platform ?? process.platform, opts?.home);
|
|
320
|
+
if (directories.length === 0) return {
|
|
321
|
+
installed: false,
|
|
322
|
+
manifestPaths: [],
|
|
323
|
+
reason: "Native bootstrap installation is supported on macOS and Linux"
|
|
324
|
+
};
|
|
325
|
+
const cliPath = opts?.cliPath ?? process.argv[1];
|
|
326
|
+
if (!cliPath) throw new Error("Unable to resolve the xopc CLI entry point");
|
|
327
|
+
const hostPath = join((opts?.cacheDir?.trim() ? assertCacheDir(opts.cacheDir) : resolveBinDir()) || resolveBinDir(), "browser-native-host");
|
|
328
|
+
mkdirSync(dirname(hostPath), { recursive: true });
|
|
329
|
+
const stateDir = opts?.stateDir ?? resolveStateDir();
|
|
330
|
+
const configPath = opts?.configPath ?? resolveConfigPath();
|
|
331
|
+
await writeTextAtomic(hostPath, `#!/bin/sh\nexec env XOPC_STATE_DIR=${shellQuote(stateDir)} XOPC_CONFIG_PATH=${shellQuote(configPath)} XOPC_LOG_CONSOLE=false ${shellQuote(opts?.nodePath ?? process.execPath)} ${shellQuote(cliPath)} browser extension native-host\n`);
|
|
332
|
+
chmodSync(hostPath, 493);
|
|
333
|
+
const manifest = JSON.stringify({
|
|
334
|
+
name: BROWSER_NATIVE_HOST_NAME,
|
|
335
|
+
description: "xopc local browser pairing bootstrap",
|
|
336
|
+
path: hostPath,
|
|
337
|
+
type: "stdio",
|
|
338
|
+
allowed_origins: [`chrome-extension://${BROWSER_EXTENSION_ID}/`]
|
|
339
|
+
}, null, 2);
|
|
340
|
+
const manifestPaths = [];
|
|
341
|
+
for (const directory of directories) {
|
|
342
|
+
mkdirSync(directory, { recursive: true });
|
|
343
|
+
const manifestPath = join(directory, `${BROWSER_NATIVE_HOST_NAME}.json`);
|
|
344
|
+
await writeTextAtomic(manifestPath, manifest);
|
|
345
|
+
manifestPaths.push(manifestPath);
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
installed: true,
|
|
349
|
+
manifestPaths
|
|
350
|
+
};
|
|
351
|
+
}
|
|
286
352
|
/** Gateway startup hook: ensure artifacts only while the extension driver is active. */
|
|
287
353
|
async function ensureBrowserExtensionOnStartup(config) {
|
|
288
354
|
if (config?.browser.enabled === false || config?.browser.driver.kind !== "extension") return;
|
|
@@ -366,4 +432,4 @@ async function openBrowserExtensionInstallUi(opts) {
|
|
|
366
432
|
};
|
|
367
433
|
}
|
|
368
434
|
//#endregion
|
|
369
|
-
export { BROWSER_EXT_REQUIRED_FILES, browserExtContentHash, browserExtDoctor, computeNeedsRefresh, ensureBrowserExtensionArtifacts, ensureBrowserExtensionOnStartup, openBrowserExtensionInstallUi, readInstalledExtensionDir, resolveBundledBrowserExtDir, resolveInstalledExtensionPath, resolveWindowsExtensionManager, validateBrowserExtLayout };
|
|
435
|
+
export { BROWSER_EXT_REQUIRED_FILES, browserExtContentHash, browserExtDoctor, browserNativeManifestDirectories, computeNeedsRefresh, ensureBrowserExtensionArtifacts, ensureBrowserExtensionOnStartup, installBrowserNativeMessagingHost, openBrowserExtensionInstallUi, readInstalledExtensionDir, resolveBundledBrowserExtDir, resolveInstalledExtensionPath, resolveWindowsExtensionManager, validateBrowserExtLayout };
|
|
@@ -31,6 +31,8 @@ export declare class ExtensionBrowserProvider {
|
|
|
31
31
|
private connected;
|
|
32
32
|
private extensionProtocolVersion;
|
|
33
33
|
private extensionVersion;
|
|
34
|
+
private connectionId;
|
|
35
|
+
private principalId;
|
|
34
36
|
private readonly config;
|
|
35
37
|
private connectionWaiters;
|
|
36
38
|
constructor(config?: ExtensionProviderConfig);
|
|
@@ -48,10 +50,12 @@ export declare class ExtensionBrowserProvider {
|
|
|
48
50
|
protocolVersion: number | null;
|
|
49
51
|
expectedProtocolVersion: number;
|
|
50
52
|
extensionVersion: string | null;
|
|
53
|
+
principalId: string | null;
|
|
51
54
|
};
|
|
52
55
|
private protocolMismatchMessage;
|
|
53
56
|
/** Shutdown the WebSocket server. */
|
|
54
57
|
shutdown(): Promise<void>;
|
|
55
58
|
private _handleMessage;
|
|
59
|
+
private _authenticateCandidate;
|
|
56
60
|
}
|
|
57
61
|
export declare function isBrowserWireResult(value: unknown): value is BrowserWireResult;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { createLogger } from "../../utils/logger/index.js";
|
|
2
2
|
import { init_logger } from "../../utils/logger.js";
|
|
3
|
+
import { deleteBrowserTabBindingsByPrincipal, getBrowserTabBindingById } from "../../storage/sqlite/browser-tab-binding-repository.js";
|
|
4
|
+
import { getDevice } from "../../storage/sqlite/device-access-repository.js";
|
|
5
|
+
import { isChromeExtensionOrigin } from "../../gateway/security/origin-check.js";
|
|
6
|
+
import { browserWireAuthenticationPayload } from "../../../packages/browser-control-contract/src/index.js";
|
|
7
|
+
import crypto from "node:crypto";
|
|
3
8
|
//#region src/browser/providers/extension.ts
|
|
4
9
|
/**
|
|
5
10
|
* Extension browser provider — connects to the xopc Chrome Extension via WebSocket.
|
|
@@ -29,6 +34,8 @@ var ExtensionBrowserProvider = class {
|
|
|
29
34
|
connected = false;
|
|
30
35
|
extensionProtocolVersion = null;
|
|
31
36
|
extensionVersion = null;
|
|
37
|
+
connectionId = null;
|
|
38
|
+
principalId = null;
|
|
32
39
|
config;
|
|
33
40
|
connectionWaiters = [];
|
|
34
41
|
constructor(config) {
|
|
@@ -62,20 +69,56 @@ var ExtensionBrowserProvider = class {
|
|
|
62
69
|
server: this.server,
|
|
63
70
|
path: "/browser-ext"
|
|
64
71
|
});
|
|
65
|
-
this.wss.on("connection", (ws) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
this.wss.on("connection", (ws, request) => {
|
|
73
|
+
const origin = request.headers.origin?.toLowerCase();
|
|
74
|
+
if (!isChromeExtensionOrigin(origin)) {
|
|
75
|
+
ws.close(4403, "Chrome extension origin required");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const challenge = {
|
|
79
|
+
type: "auth_challenge",
|
|
80
|
+
protocolVersion: 4,
|
|
81
|
+
connectionId: crypto.randomUUID(),
|
|
82
|
+
challenge: crypto.randomBytes(32).toString("base64url"),
|
|
83
|
+
issuedAt: Date.now()
|
|
84
|
+
};
|
|
85
|
+
let authenticated = false;
|
|
86
|
+
const authenticationTimer = setTimeout(() => {
|
|
87
|
+
if (!authenticated) ws.close(4401, "Browser authentication timed out");
|
|
88
|
+
}, 5e3);
|
|
89
|
+
ws.send(JSON.stringify(challenge));
|
|
77
90
|
ws.on("message", (data) => {
|
|
78
|
-
|
|
91
|
+
if (authenticated) {
|
|
92
|
+
this._handleMessage(data.toString(), ws);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!this._authenticateCandidate(data.toString(), origin, challenge)) {
|
|
96
|
+
clearTimeout(authenticationTimer);
|
|
97
|
+
ws.close(4401, "Browser authentication failed");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
authenticated = true;
|
|
101
|
+
clearTimeout(authenticationTimer);
|
|
102
|
+
const message = JSON.parse(data.toString());
|
|
103
|
+
const previousClient = this.clientWs;
|
|
104
|
+
this.clientWs = ws;
|
|
105
|
+
this.socketConnected = true;
|
|
106
|
+
this.handshakeReceived = true;
|
|
107
|
+
this.connected = true;
|
|
108
|
+
this.extensionProtocolVersion = message.protocolVersion;
|
|
109
|
+
this.extensionVersion = message.extensionVersion;
|
|
110
|
+
this.connectionId = message.connectionId;
|
|
111
|
+
this.principalId = message.principalId;
|
|
112
|
+
if (previousClient && previousClient !== ws) try {
|
|
113
|
+
previousClient.close(4001, "Connection replaced");
|
|
114
|
+
} catch {}
|
|
115
|
+
for (const waiter of this.connectionWaiters) waiter.resolve();
|
|
116
|
+
this.connectionWaiters = [];
|
|
117
|
+
log.info({
|
|
118
|
+
principalId: message.principalId,
|
|
119
|
+
extensionId: message.extensionId,
|
|
120
|
+
connectionId: message.connectionId
|
|
121
|
+
}, "Chrome extension authenticated");
|
|
79
122
|
});
|
|
80
123
|
ws.on("close", (code, reason) => {
|
|
81
124
|
if (this.clientWs !== ws) return;
|
|
@@ -84,12 +127,16 @@ var ExtensionBrowserProvider = class {
|
|
|
84
127
|
reason: reason.toString(),
|
|
85
128
|
pendingCount: this.pending.size
|
|
86
129
|
}, "Chrome Extension disconnected");
|
|
130
|
+
const disconnectedPrincipalId = this.principalId;
|
|
87
131
|
this.clientWs = null;
|
|
88
132
|
this.socketConnected = false;
|
|
89
133
|
this.handshakeReceived = false;
|
|
90
134
|
this.connected = false;
|
|
91
135
|
this.extensionProtocolVersion = null;
|
|
92
136
|
this.extensionVersion = null;
|
|
137
|
+
this.connectionId = null;
|
|
138
|
+
this.principalId = null;
|
|
139
|
+
if (disconnectedPrincipalId) deleteBrowserTabBindingsByPrincipal(disconnectedPrincipalId);
|
|
93
140
|
for (const [id, req] of this.pending) {
|
|
94
141
|
clearTimeout(req.timer);
|
|
95
142
|
req.reject(/* @__PURE__ */ new Error("Extension disconnected"));
|
|
@@ -145,7 +192,7 @@ var ExtensionBrowserProvider = class {
|
|
|
145
192
|
/** Wait for the Chrome Extension to connect. */
|
|
146
193
|
async waitForConnection(timeoutMs) {
|
|
147
194
|
if (this.connected) return;
|
|
148
|
-
if (this.socketConnected && this.handshakeReceived && this.extensionProtocolVersion !==
|
|
195
|
+
if (this.socketConnected && this.handshakeReceived && this.extensionProtocolVersion !== 4) throw new Error(this.protocolMismatchMessage());
|
|
149
196
|
const timeout = timeoutMs ?? this.config.connectionTimeout;
|
|
150
197
|
return new Promise((resolve, reject) => {
|
|
151
198
|
const timer = setTimeout(() => {
|
|
@@ -171,10 +218,15 @@ var ExtensionBrowserProvider = class {
|
|
|
171
218
|
const detail = this.socketConnected ? this.protocolMismatchMessage() : "Extension not connected. Ensure the Chrome Extension is installed and connected.";
|
|
172
219
|
throw new Error(detail);
|
|
173
220
|
}
|
|
221
|
+
if (input.target?.kind === "attached_tab") {
|
|
222
|
+
const binding = getBrowserTabBindingById(input.target.bindingId);
|
|
223
|
+
if (!binding || binding.principalId !== this.principalId) throw new Error("Attached tab binding does not belong to the connected browser");
|
|
224
|
+
}
|
|
174
225
|
const id = `cmd_${++this.commandCounter}_${Date.now()}`;
|
|
175
226
|
const cmd = {
|
|
176
227
|
id,
|
|
177
|
-
protocolVersion:
|
|
228
|
+
protocolVersion: 4,
|
|
229
|
+
connectionId: this.connectionId,
|
|
178
230
|
input,
|
|
179
231
|
timeoutMs: timeoutMs ?? this.config.commandTimeout,
|
|
180
232
|
visualFallback
|
|
@@ -207,12 +259,13 @@ var ExtensionBrowserProvider = class {
|
|
|
207
259
|
socketConnected: this.socketConnected,
|
|
208
260
|
connected: this.connected,
|
|
209
261
|
protocolVersion: this.extensionProtocolVersion,
|
|
210
|
-
expectedProtocolVersion:
|
|
211
|
-
extensionVersion: this.extensionVersion
|
|
262
|
+
expectedProtocolVersion: 4,
|
|
263
|
+
extensionVersion: this.extensionVersion,
|
|
264
|
+
principalId: this.principalId
|
|
212
265
|
};
|
|
213
266
|
}
|
|
214
267
|
protocolMismatchMessage() {
|
|
215
|
-
return `Browser extension protocol mismatch (expected
|
|
268
|
+
return `Browser extension protocol mismatch (expected 4, received ${this.extensionProtocolVersion ?? "unknown"}). Reload the extension in Chrome.`;
|
|
216
269
|
}
|
|
217
270
|
/** Shutdown the WebSocket server. */
|
|
218
271
|
async shutdown() {
|
|
@@ -246,32 +299,19 @@ var ExtensionBrowserProvider = class {
|
|
|
246
299
|
this.handshakeReceived = false;
|
|
247
300
|
this.extensionProtocolVersion = null;
|
|
248
301
|
this.extensionVersion = null;
|
|
302
|
+
this.connectionId = null;
|
|
303
|
+
this.principalId = null;
|
|
249
304
|
log.info("Extension provider shut down");
|
|
250
305
|
}
|
|
251
306
|
_handleMessage(raw, sourceWs) {
|
|
252
307
|
if (sourceWs !== this.clientWs) return;
|
|
253
308
|
try {
|
|
254
309
|
const msg = JSON.parse(raw);
|
|
310
|
+
if (!this.connectionId || msg.connectionId !== this.connectionId) return;
|
|
255
311
|
if (msg.type === "status") {
|
|
256
312
|
const status = msg;
|
|
257
|
-
this.
|
|
258
|
-
|
|
259
|
-
this.extensionVersion = typeof status.extensionVersion === "string" ? status.extensionVersion : null;
|
|
260
|
-
this.connected = status.protocolVersion === 3;
|
|
261
|
-
if (this.connected) {
|
|
262
|
-
for (const waiter of this.connectionWaiters) waiter.resolve();
|
|
263
|
-
this.connectionWaiters = [];
|
|
264
|
-
log.debug(msg, "Compatible browser extension connected");
|
|
265
|
-
} else {
|
|
266
|
-
const error = new Error(this.protocolMismatchMessage());
|
|
267
|
-
for (const waiter of this.connectionWaiters) waiter.reject(error);
|
|
268
|
-
this.connectionWaiters = [];
|
|
269
|
-
log.warn({
|
|
270
|
-
expectedProtocolVersion: 3,
|
|
271
|
-
extensionProtocolVersion: this.extensionProtocolVersion,
|
|
272
|
-
extensionVersion: this.extensionVersion
|
|
273
|
-
}, "Browser extension protocol mismatch; reload the extension");
|
|
274
|
-
}
|
|
313
|
+
if (status.principalId !== this.principalId) return;
|
|
314
|
+
log.debug({ sessionCount: status.sessionCount }, "Browser extension status updated");
|
|
275
315
|
return;
|
|
276
316
|
}
|
|
277
317
|
if (isBrowserWireKeepAlive(msg)) return;
|
|
@@ -297,14 +337,40 @@ var ExtensionBrowserProvider = class {
|
|
|
297
337
|
log.error({ err: e }, "Failed to parse extension message");
|
|
298
338
|
}
|
|
299
339
|
}
|
|
340
|
+
_authenticateCandidate(raw, origin, challenge) {
|
|
341
|
+
try {
|
|
342
|
+
const message = JSON.parse(raw);
|
|
343
|
+
if (message.type !== "authenticate" || message.protocolVersion !== 4 || message.connectionId !== challenge.connectionId || `chrome-extension://${message.extensionId}` !== origin || Math.abs(Date.now() - challenge.issuedAt) > 5e3) return false;
|
|
344
|
+
const device = getDevice(message.principalId);
|
|
345
|
+
if (!device || device.revokedAt !== void 0 || device.platform !== "chrome" || device.extensionId !== message.extensionId) return false;
|
|
346
|
+
const payload = browserWireAuthenticationPayload({
|
|
347
|
+
protocolVersion: message.protocolVersion,
|
|
348
|
+
connectionId: message.connectionId,
|
|
349
|
+
challenge: challenge.challenge,
|
|
350
|
+
issuedAt: challenge.issuedAt,
|
|
351
|
+
principalId: message.principalId,
|
|
352
|
+
extensionId: message.extensionId,
|
|
353
|
+
extensionVersion: message.extensionVersion
|
|
354
|
+
});
|
|
355
|
+
return crypto.verify("sha256", Buffer.from(payload), {
|
|
356
|
+
key: crypto.createPublicKey({
|
|
357
|
+
key: device.publicKeyJwk,
|
|
358
|
+
format: "jwk"
|
|
359
|
+
}),
|
|
360
|
+
dsaEncoding: "ieee-p1363"
|
|
361
|
+
}, Buffer.from(message.signature, "base64url"));
|
|
362
|
+
} catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
300
366
|
};
|
|
301
367
|
function isBrowserWireResult(value) {
|
|
302
368
|
if (!value || typeof value !== "object") return false;
|
|
303
369
|
const result = value.result;
|
|
304
|
-
return typeof value.id === "string" && Boolean(result) && typeof result === "object" && typeof result.ok === "boolean";
|
|
370
|
+
return typeof value.id === "string" && typeof value.connectionId === "string" && Boolean(result) && typeof result === "object" && typeof result.ok === "boolean";
|
|
305
371
|
}
|
|
306
372
|
function isBrowserWireKeepAlive(value) {
|
|
307
|
-
return Boolean(value) && typeof value === "object" && value.type === "keepalive" && typeof value.timestamp === "number";
|
|
373
|
+
return Boolean(value) && typeof value === "object" && value.type === "keepalive" && typeof value.connectionId === "string" && typeof value.timestamp === "number";
|
|
308
374
|
}
|
|
309
375
|
//#endregion
|
|
310
376
|
export { ExtensionBrowserProvider, isBrowserWireResult };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BrowserActionInput, BrowserControlResult } from '@xopcai/browser-control-contract';
|
|
1
|
+
import type { BrowserActionInput, BrowserControlResult, BrowserTarget } from '@xopcai/browser-control-contract';
|
|
2
2
|
import type { Config } from '../../config/schema.js';
|
|
3
3
|
import type { BrowserDriver } from '../drivers/browser-driver.js';
|
|
4
4
|
export interface BrowserRuntimeOptions {
|
|
@@ -6,6 +6,7 @@ export interface BrowserRuntimeOptions {
|
|
|
6
6
|
createDriver: () => Promise<BrowserDriver>;
|
|
7
7
|
allowedUploadRoots?: string[];
|
|
8
8
|
emit?: (type: string, payload: unknown) => void;
|
|
9
|
+
resolveTarget?: (taskKey: string) => BrowserTarget | undefined;
|
|
9
10
|
}
|
|
10
11
|
export declare class BrowserRuntime {
|
|
11
12
|
private readonly options;
|
|
@@ -14,10 +14,14 @@ var BrowserRuntime = class {
|
|
|
14
14
|
this.evictExpiredSessions();
|
|
15
15
|
if (!this.options.getConfig().enabled) return failure("DRIVER_UNAVAILABLE", "Browser Control is disabled.");
|
|
16
16
|
if (signal?.aborted) return failure("ABORTED", "Operation was aborted.");
|
|
17
|
+
const effectiveInput = input.target ? input : {
|
|
18
|
+
...input,
|
|
19
|
+
target: this.options.resolveTarget?.(taskKey)
|
|
20
|
+
};
|
|
17
21
|
let session;
|
|
18
22
|
let driver;
|
|
19
23
|
try {
|
|
20
|
-
session = await this.resolveSession(taskKey,
|
|
24
|
+
session = await this.resolveSession(taskKey, effectiveInput.sessionId);
|
|
21
25
|
if (!session) return failure("SESSION_NOT_FOUND", "The browser session does not belong to this task.");
|
|
22
26
|
driver = await this.getDriver();
|
|
23
27
|
} catch (error) {
|
|
@@ -30,7 +34,7 @@ var BrowserRuntime = class {
|
|
|
30
34
|
});
|
|
31
35
|
let result;
|
|
32
36
|
try {
|
|
33
|
-
result = await this.dispatch(driver, session,
|
|
37
|
+
result = await this.dispatch(driver, session, effectiveInput, signal);
|
|
34
38
|
result = this.constrainResult(result);
|
|
35
39
|
} catch (error) {
|
|
36
40
|
result = failure("DRIVER_UNAVAILABLE", error instanceof Error ? error.message : String(error));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { register } from "../registry.js";
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
//#region src/cli/commands/browser.ts
|
|
4
|
-
function createBrowserCommand(
|
|
4
|
+
function createBrowserCommand(ctx) {
|
|
5
5
|
const command = new Command("browser").description("Browser Control v2 diagnostics and extension setup");
|
|
6
6
|
command.command("doctor").description("Check the configured browser driver").action(async () => {
|
|
7
7
|
const { doctorCli } = await import("./browser-cli-helpers.js");
|
|
@@ -13,8 +13,18 @@ function createBrowserCommand(_ctx) {
|
|
|
13
13
|
console.log(JSON.stringify(await browserExtDoctor(), null, 2));
|
|
14
14
|
});
|
|
15
15
|
extension.command("install").description("Install extension artifacts").action(async () => {
|
|
16
|
-
const { ensureBrowserExtensionArtifacts } = await import("../../browser/providers/browser-ext-install.js");
|
|
17
|
-
|
|
16
|
+
const { ensureBrowserExtensionArtifacts, installBrowserNativeMessagingHost } = await import("../../browser/providers/browser-ext-install.js");
|
|
17
|
+
const artifacts = await ensureBrowserExtensionArtifacts();
|
|
18
|
+
const nativeHost = await installBrowserNativeMessagingHost({ configPath: ctx.configPath });
|
|
19
|
+
console.log(JSON.stringify({
|
|
20
|
+
...artifacts,
|
|
21
|
+
nativeHost
|
|
22
|
+
}, null, 2));
|
|
23
|
+
});
|
|
24
|
+
extension.command("native-host").description("Run the Chrome native messaging bootstrap host").action(async () => {
|
|
25
|
+
process.env.XOPC_LOG_CONSOLE = "false";
|
|
26
|
+
const { runBrowserNativeMessagingHost } = await import("../../browser/native-messaging-host.js");
|
|
27
|
+
await runBrowserNativeMessagingHost(ctx.configPath);
|
|
18
28
|
});
|
|
19
29
|
return command;
|
|
20
30
|
}
|
|
@@ -52,7 +52,7 @@ async function checkDatabaseSchema(ctx) {
|
|
|
52
52
|
id: "database-schema",
|
|
53
53
|
label: "Database schema",
|
|
54
54
|
status: "pass",
|
|
55
|
-
message: `SQLite schema v${status.dbVersion} (current release
|
|
55
|
+
message: `SQLite schema v${status.dbVersion} (current release v156).`,
|
|
56
56
|
hints: []
|
|
57
57
|
};
|
|
58
58
|
}
|
|
@@ -2,6 +2,7 @@ import { resolveStateDir } from "../config/paths-state.js";
|
|
|
2
2
|
import { createLogger } from "../utils/logger/index.js";
|
|
3
3
|
import { init_logger } from "../utils/logger.js";
|
|
4
4
|
import { deleteEndpointSessionBinding, getEndpointSessionBinding, setEndpointSessionBinding } from "../storage/sqlite/endpoint-session-binding-repository.js";
|
|
5
|
+
import { deleteBrowserTabBindingsByEndpoint } from "../storage/sqlite/browser-tab-binding-repository.js";
|
|
5
6
|
import { bindEndpointPrincipal, getEndpointPrincipal, touchEndpointPrincipal } from "../storage/sqlite/endpoint-principal-repository.js";
|
|
6
7
|
import { finishEndpointToolInvocationAudit, startEndpointToolInvocationAudit } from "../storage/sqlite/endpoint-tool-invocation-repository.js";
|
|
7
8
|
import "../storage/sqlite/index.js";
|
|
@@ -75,6 +76,7 @@ var EndpointToolRuntime = class {
|
|
|
75
76
|
remove(endpointId, connectionId) {
|
|
76
77
|
if (!this.registry.remove(endpointId, connectionId)) return;
|
|
77
78
|
this.invocations.failEndpoint(endpointId);
|
|
79
|
+
deleteBrowserTabBindingsByEndpoint(endpointId);
|
|
78
80
|
log.info({
|
|
79
81
|
endpointId,
|
|
80
82
|
connectionId
|
|
@@ -56,7 +56,7 @@ var ChatStreamMapper = class {
|
|
|
56
56
|
status: event.status === "error" ? "error" : "complete",
|
|
57
57
|
...typeof event.message === "string" && event.message ? { message: event.message } : {}
|
|
58
58
|
})];
|
|
59
|
-
case "user_message": return [this.make("user_message", { message: userMessageFromEvent(event) })];
|
|
59
|
+
case "user_message": return [this.make("user_message", { message: userMessageFromEvent(event, this.opts.runId) })];
|
|
60
60
|
case "user_transcript": return [this.make("user_transcript", {
|
|
61
61
|
text: String(event.text ?? ""),
|
|
62
62
|
media: event.media
|
|
@@ -427,9 +427,10 @@ var ChatStreamMapper = class {
|
|
|
427
427
|
}
|
|
428
428
|
}
|
|
429
429
|
};
|
|
430
|
-
function userMessageFromEvent(event) {
|
|
430
|
+
function userMessageFromEvent(event, turnId) {
|
|
431
431
|
return {
|
|
432
432
|
role: "user",
|
|
433
|
+
turnId,
|
|
433
434
|
content: event.content ?? [],
|
|
434
435
|
attachments: event.media,
|
|
435
436
|
metadata: userMessageDisplayMetadata(event.metadata),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createLogger } from "../../utils/logger/index.js";
|
|
2
2
|
import { init_logger } from "../../utils/logger.js";
|
|
3
|
+
import { listDevices } from "../../storage/sqlite/device-access-repository.js";
|
|
3
4
|
import { resolveAllowedBrowserOrigins, resolveGatewayServiceListenPort } from "../host.js";
|
|
4
5
|
import { resolveGatewayEffectiveHost } from "../../config/gateway-bind.js";
|
|
5
6
|
import { loadTunnelState } from "../../tunnel/tunnel-state.js";
|
|
@@ -7,7 +8,7 @@ import { resolveReverseProxyPublicUrl } from "../public-url.js";
|
|
|
7
8
|
import { maxSessionInputRequestBodyBytes } from "../chat-limits.js";
|
|
8
9
|
import { isLoopbackIpAddress, isTrustedProxyAddress } from "../client-ip.js";
|
|
9
10
|
import { buildGatewayConsoleCspHeader } from "../security/csp.js";
|
|
10
|
-
import { checkBrowserOrigin } from "../security/origin-check.js";
|
|
11
|
+
import { checkBrowserOrigin, isChromeExtensionOrigin } from "../security/origin-check.js";
|
|
11
12
|
import { auth } from "./middleware/auth.js";
|
|
12
13
|
import { gatewayScopes } from "./middleware/scopes.js";
|
|
13
14
|
import { createChannelRateLimitMiddleware, createChatRateLimitMiddleware, createMediaRateLimitMiddleware, createStrictRateLimitMiddleware, createTaskRateLimitMiddleware, createXopcCloudPollRateLimitMiddleware } from "./middleware/strict-rate-limit.js";
|
|
@@ -52,6 +53,19 @@ function createHonoApp(config) {
|
|
|
52
53
|
tunnelPublicUrl: loadTunnelState()?.publicUrl,
|
|
53
54
|
reverseProxyPublicUrl: resolveReverseProxyPublicUrl(service.currentConfig)
|
|
54
55
|
});
|
|
56
|
+
let loggedUnavailableDeviceStore = false;
|
|
57
|
+
const resolvePairedBrowserExtensionOrigins = () => {
|
|
58
|
+
try {
|
|
59
|
+
return listDevices().filter((device) => device.platform === "chrome" && !device.revokedAt && device.extensionId).map((device) => `chrome-extension://${device.extensionId}`);
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
if (!loggedUnavailableDeviceStore) {
|
|
62
|
+
loggedUnavailableDeviceStore = true;
|
|
63
|
+
log.warn({ err: cause }, "Browser extension origins unavailable until the device store opens");
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const resolveAllBrowserOrigins = () => [...resolveBrowserOrigins(), ...resolvePairedBrowserExtensionOrigins()];
|
|
55
69
|
/**
|
|
56
70
|
* TCP source for the in-flight request, normalized for trusted-proxy checks.
|
|
57
71
|
* Returns undefined when the runtime doesn't expose conninfo (tests, mocks).
|
|
@@ -84,9 +98,10 @@ function createHonoApp(config) {
|
|
|
84
98
|
registerSiteShareMiddleware(app, service);
|
|
85
99
|
app.use(cors({
|
|
86
100
|
origin: (origin) => {
|
|
87
|
-
const allowed =
|
|
101
|
+
const allowed = resolveAllBrowserOrigins();
|
|
88
102
|
if (!origin) return allowed[0] ?? `http://127.0.0.1:${gatewayPort}`;
|
|
89
103
|
const normalized = origin.toLowerCase();
|
|
104
|
+
if (isChromeExtensionOrigin(normalized)) return origin;
|
|
90
105
|
if (allowed.find((entry) => entry.toLowerCase() === normalized)) return origin;
|
|
91
106
|
return allowed.includes("*") ? "*" : "";
|
|
92
107
|
},
|
|
@@ -123,10 +138,11 @@ function createHonoApp(config) {
|
|
|
123
138
|
if (isExtensionGatewayUiAssetPath(c.req.path)) return next();
|
|
124
139
|
const origin = c.req.header("origin");
|
|
125
140
|
if (!origin || origin.trim().toLowerCase() === "null") return next();
|
|
141
|
+
if (isChromeExtensionOrigin(origin) && (c.req.path.startsWith("/api/device-pairing/") || c.req.path === "/api/device-auth/refresh")) return next();
|
|
126
142
|
const result = checkBrowserOrigin({
|
|
127
143
|
requestHost: c.req.header("host"),
|
|
128
144
|
origin,
|
|
129
|
-
allowedOrigins:
|
|
145
|
+
allowedOrigins: resolveAllBrowserOrigins(),
|
|
130
146
|
allowHostHeaderOriginFallback,
|
|
131
147
|
autoAllowSameHostFromTrustedProxy: isRequestFromTrustedProxy(c),
|
|
132
148
|
isLocalClient: false
|
|
@@ -19,4 +19,7 @@ export declare function auth(config?: AuthConfig): import("hono").MiddlewareHand
|
|
|
19
19
|
}, 429, "json">) | (Response & import("hono").TypedResponse<{
|
|
20
20
|
error: string;
|
|
21
21
|
code: string;
|
|
22
|
-
}, 401, "json">)
|
|
22
|
+
}, 401, "json">) | (Response & import("hono").TypedResponse<{
|
|
23
|
+
error: string;
|
|
24
|
+
code: string;
|
|
25
|
+
}, 403, "json">)>;
|