@ixo/oracle-runtime-workers 0.2.0
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/README.md +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- package/src/tasks/store.ts +351 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
UCAN_STORE_DEFAULT_URLS,
|
|
4
|
+
VFS_DEFAULT_BASE_URLS,
|
|
5
|
+
} from '../../owner-store/ixo-vfs-store';
|
|
6
|
+
import { OraclePlugin } from '../../plugin-api/oracle-plugin';
|
|
7
|
+
import type {
|
|
8
|
+
PluginManifest,
|
|
9
|
+
PluginTool,
|
|
10
|
+
RuntimeContext,
|
|
11
|
+
} from '../../plugin-api/types';
|
|
12
|
+
import type { SandboxMcpClientFactory } from '../sandbox/sandbox.plugin';
|
|
13
|
+
import { createVfsSandboxTools } from './vfs-sandbox-tools';
|
|
14
|
+
import { createVfsTools } from './vfs-tools';
|
|
15
|
+
|
|
16
|
+
/** Optional tuning env this plugin owns (the worker URLs come from NETWORK). */
|
|
17
|
+
const configSchema = z.object({
|
|
18
|
+
VFS_MAX_READ_LINES: z.coerce.number().int().positive().default(2000),
|
|
19
|
+
VFS_REQUEST_TIMEOUT_MS: z.coerce.number().int().positive().default(20000),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sibling env the VFS plugin reads but does not own. `NETWORK` (owned by the
|
|
24
|
+
* base runtime schema) selects the worker URLs; `VFS_BASE_URL` /
|
|
25
|
+
* `UCAN_STORE_URL` (also base-schema keys, used by the VFS owner store) act
|
|
26
|
+
* as per-deployment overrides; `SANDBOX_MCP_URL` (owned by the sandbox
|
|
27
|
+
* plugin), when set, adds the two sandbox↔files bridge tools so a file can
|
|
28
|
+
* move between the two without its bytes passing through the LLM.
|
|
29
|
+
*/
|
|
30
|
+
const siblingEnvSchema = z.object({
|
|
31
|
+
NETWORK: z.enum(['mainnet', 'testnet', 'devnet']).optional(),
|
|
32
|
+
VFS_BASE_URL: z.string().url().optional(),
|
|
33
|
+
UCAN_STORE_URL: z.string().url().optional(),
|
|
34
|
+
SANDBOX_MCP_URL: z.string().url().optional(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** Runtime VFS config the client + tools consume. URLs derived from NETWORK. */
|
|
38
|
+
export interface VfsConfig {
|
|
39
|
+
VFS_BASE_URL: string;
|
|
40
|
+
UCAN_STORE_URL: string;
|
|
41
|
+
VFS_MAX_READ_LINES: number;
|
|
42
|
+
VFS_REQUEST_TIMEOUT_MS: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const manifest: PluginManifest = {
|
|
46
|
+
title: 'Files',
|
|
47
|
+
summary:
|
|
48
|
+
"The user's Virtual Filesystem (VFS) — their persistent, secure, governed filesystem: the canonical home for their real documents, notes, datasets, and artifacts, in folders, searchable and versioned linked to their domain. Reusable context that lives across sessions — not scratch files, not chat attachments, not the web. Securely stored and access-controlled (managed encryption); authorized IXO services can read content to power search, previews, and these tools, so it is NOT end-to-end encrypted — never describe it as zero-knowledge or unreadable by IXO.",
|
|
49
|
+
category: 'data',
|
|
50
|
+
// On-demand with two exceptions: `vfs_search` and `vfs_read` carry per-tool
|
|
51
|
+
// `visibility: 'always'` so recall ("what did my notes say…") works with no
|
|
52
|
+
// load step. The write/organise/share surface (and the sandbox bridges)
|
|
53
|
+
// loads via `load_capability(['vfs'])` — keeping ~11 tool schemas out of
|
|
54
|
+
// every unrelated chat turn.
|
|
55
|
+
visibility: 'on-demand',
|
|
56
|
+
whenToUse: [
|
|
57
|
+
"The user refers to a document, note, file, folder, or something they 'saved', 'uploaded', or 'shared with you'.",
|
|
58
|
+
'You need to create, update, or organise a file for the user (draft, report, notes, export) that should persist.',
|
|
59
|
+
'You need to find or quote something from the user\'s files ("what did my notes say about X", "find the contract").',
|
|
60
|
+
'You can reuse a file the user already has instead of asking them to paste or re-upload it.',
|
|
61
|
+
'The user asks you to share a file or make it downloadable.',
|
|
62
|
+
"Persist something the sandbox produced (a report, export, chart, or dataset under /workspace/…) into the user's files so it survives the session — use `sandbox_to_vfs`.",
|
|
63
|
+
"Feed one of the user's files into the sandbox so `sandbox_run` code can process it — use `vfs_to_sandbox` (writes under /workspace/data/).",
|
|
64
|
+
'Remember you act ONLY within the files and rights the user granted you. If a file action reports you have no access (or only read-only), relay the short grant steps the tool returns — with your agent DID — so the user can authorize you, then retry. Never claim a file is missing when the real issue is access.',
|
|
65
|
+
],
|
|
66
|
+
whenNotToUse: [
|
|
67
|
+
"General knowledge or web questions — the files are the user's private content, not a knowledge base of the world.",
|
|
68
|
+
"Content the user pasted directly into chat (act on that inline; don't write it to a file unless asked).",
|
|
69
|
+
"Temporary/scratch or intermediate files produced during a code or compute run — those belong to the sandbox, not the user's filesystem.",
|
|
70
|
+
'Another plugin owns the data (flows, skills, memory) — use that plugin.',
|
|
71
|
+
],
|
|
72
|
+
examples: [
|
|
73
|
+
{
|
|
74
|
+
user: 'What did my project notes say about the launch date?',
|
|
75
|
+
thought: "Search the user's files, then read the relevant lines.",
|
|
76
|
+
tool: 'vfs_search',
|
|
77
|
+
args: { q: 'launch date', path: '/' },
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
user: 'Save this summary as notes/meeting-2026-07.md',
|
|
81
|
+
thought: 'Create the file at that path.',
|
|
82
|
+
tool: 'vfs_write',
|
|
83
|
+
args: { path: '/notes/meeting-2026-07.md' },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
user: 'In todo.md change "Draft" to "Final"',
|
|
87
|
+
thought: 'Exact-string edit.',
|
|
88
|
+
tool: 'vfs_edit',
|
|
89
|
+
args: { path: '/todo.md', oldString: 'Draft', newString: 'Final' },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
user: 'Share my resume so I can send a link',
|
|
93
|
+
thought: 'Publish it and return the link.',
|
|
94
|
+
tool: 'vfs_share',
|
|
95
|
+
args: { path: '/resume.pdf', public: true },
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Optional dependency injection — primarily for tests. */
|
|
101
|
+
export interface VfsPluginOptions {
|
|
102
|
+
/** Injected transport for the VFS client. Defaults to global `fetch`. */
|
|
103
|
+
fetchImpl?: typeof fetch;
|
|
104
|
+
/** Retry backoff forwarded to the VFS client. Tests pass `0`. */
|
|
105
|
+
retryDelayMs?: number;
|
|
106
|
+
/** Injected sandbox MCP-client factory for the two bridge tools. */
|
|
107
|
+
sandboxMcpClientFactory?: SandboxMcpClientFactory;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* VFS plugin.
|
|
112
|
+
*
|
|
113
|
+
* Contributes ten file tools (`vfs_search`, `vfs_grep`, `vfs_glob`,
|
|
114
|
+
* `vfs_list`, `vfs_read`, `vfs_write`, `vfs_edit`, `vfs_move`, `vfs_delete`,
|
|
115
|
+
* `vfs_share`) that read, create, edit, search, organise, and share a user's
|
|
116
|
+
* files on the IXO Virtual Filesystem. The oracle acts as the user, inside the
|
|
117
|
+
* folder the user delegated to it; each tool resolves a fresh single-use UCAN
|
|
118
|
+
* bearer per call.
|
|
119
|
+
*
|
|
120
|
+
* When the sandbox is also configured (`SANDBOX_MCP_URL`, a sibling env owned
|
|
121
|
+
* by the sandbox plugin), two extra bridge tools are added — `sandbox_to_vfs`
|
|
122
|
+
* and `vfs_to_sandbox` — that move a file between the user's sandbox and their
|
|
123
|
+
* files entirely server-side, so the bytes never pass through the LLM.
|
|
124
|
+
*
|
|
125
|
+
* Tools are request-time (`getRequestTools`) because auth needs the live
|
|
126
|
+
* `rtCtx.user.did` and the user's deposited delegation. With no signing key,
|
|
127
|
+
* or invalid config, the plugin contributes nothing and the agent simply
|
|
128
|
+
* doesn't see the filesystem that turn. Always on — no `autoDetect`.
|
|
129
|
+
*/
|
|
130
|
+
export class VfsPlugin extends OraclePlugin {
|
|
131
|
+
readonly name = 'vfs';
|
|
132
|
+
|
|
133
|
+
readonly version = '1.0.0';
|
|
134
|
+
|
|
135
|
+
readonly manifest = manifest;
|
|
136
|
+
|
|
137
|
+
override readonly configSchema = configSchema;
|
|
138
|
+
|
|
139
|
+
private readonly options: VfsPluginOptions;
|
|
140
|
+
|
|
141
|
+
constructor(options: VfsPluginOptions = {}) {
|
|
142
|
+
super();
|
|
143
|
+
this.options = options;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
override getRequestTools(rtCtx: RuntimeContext): PluginTool[] {
|
|
147
|
+
if (!rtCtx.ucan.hasSigningKey()) {
|
|
148
|
+
rtCtx.logger.warn('[vfs] skipping — oracle has no UCAN signing key.');
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const tuning = configSchema.safeParse(rtCtx.config);
|
|
153
|
+
const siblings = siblingEnvSchema.safeParse(rtCtx.config);
|
|
154
|
+
const network = (siblings.success && siblings.data.NETWORK) || 'mainnet';
|
|
155
|
+
const cfg: VfsConfig = {
|
|
156
|
+
VFS_BASE_URL:
|
|
157
|
+
(siblings.success && siblings.data.VFS_BASE_URL) ||
|
|
158
|
+
VFS_DEFAULT_BASE_URLS[network] ||
|
|
159
|
+
// Unreachable: `network` is one of the three keys the map declares.
|
|
160
|
+
'https://vfs.ixo.earth',
|
|
161
|
+
UCAN_STORE_URL:
|
|
162
|
+
(siblings.success && siblings.data.UCAN_STORE_URL) ||
|
|
163
|
+
UCAN_STORE_DEFAULT_URLS[network] ||
|
|
164
|
+
'https://store.ucan.ixo.earth',
|
|
165
|
+
VFS_MAX_READ_LINES: tuning.success
|
|
166
|
+
? tuning.data.VFS_MAX_READ_LINES
|
|
167
|
+
: 2000,
|
|
168
|
+
VFS_REQUEST_TIMEOUT_MS: tuning.success
|
|
169
|
+
? tuning.data.VFS_REQUEST_TIMEOUT_MS
|
|
170
|
+
: 20000,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const tools = createVfsTools({
|
|
174
|
+
cfg,
|
|
175
|
+
fetchImpl: this.options.fetchImpl,
|
|
176
|
+
retryDelayMs: this.options.retryDelayMs,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// When the sandbox is configured, add the two sandbox↔files bridge tools.
|
|
180
|
+
// The sandbox MCP client is built lazily inside each handler, so adding
|
|
181
|
+
// them costs nothing until one is actually called.
|
|
182
|
+
const sandboxMcpUrl = siblings.success
|
|
183
|
+
? siblings.data.SANDBOX_MCP_URL
|
|
184
|
+
: undefined;
|
|
185
|
+
if (sandboxMcpUrl) {
|
|
186
|
+
tools.push(
|
|
187
|
+
...createVfsSandboxTools(
|
|
188
|
+
{
|
|
189
|
+
vfsCfg: cfg,
|
|
190
|
+
sandboxMcpUrl,
|
|
191
|
+
vfsFetchImpl: this.options.fetchImpl,
|
|
192
|
+
vfsRetryDelayMs: this.options.retryDelayMs,
|
|
193
|
+
},
|
|
194
|
+
this.options.sandboxMcpClientFactory,
|
|
195
|
+
),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return tools;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending frontend (browser-tool / AG-UI action) calls awaiting a result
|
|
3
|
+
* from the user's browser — the Workers counterpart of the Node runtime's
|
|
4
|
+
* `callFrontendTool`, which parks a promise on `rootEventEmitter` until the
|
|
5
|
+
* socket delivers `browser_tool_result` / `action_call_result` for the same
|
|
6
|
+
* `toolCallId`.
|
|
7
|
+
*
|
|
8
|
+
* Settlement rules are Node's, verbatim:
|
|
9
|
+
* - `error` set → reject(Error(error))
|
|
10
|
+
* - AG-UI and `result.success === false` → reject(Error(result.error ?? 'Action failed'))
|
|
11
|
+
* - otherwise → resolve(result)
|
|
12
|
+
* - no result within `timeoutMs` → reject("<kind> timeout after <ms>ms: <tool>")
|
|
13
|
+
*
|
|
14
|
+
* Purely in-memory: a pending call does not survive an object restart, and
|
|
15
|
+
* neither does the turn that made it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type FrontendCallKind = 'browser' | 'agui';
|
|
19
|
+
|
|
20
|
+
export interface FrontendCallResult {
|
|
21
|
+
toolCallId: string;
|
|
22
|
+
result?: unknown;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PendingFrontendCall {
|
|
27
|
+
kind: FrontendCallKind;
|
|
28
|
+
toolCallId: string;
|
|
29
|
+
toolName: string;
|
|
30
|
+
sessionId: string;
|
|
31
|
+
startedAt: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Pending extends PendingFrontendCall {
|
|
35
|
+
resolve: (value: unknown) => void;
|
|
36
|
+
reject: (error: Error) => void;
|
|
37
|
+
timer: ReturnType<typeof setTimeout>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const KIND_LABEL: Record<FrontendCallKind, string> = {
|
|
41
|
+
browser: 'Browser tool',
|
|
42
|
+
agui: 'AG-UI action',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return typeof value === 'object' && value !== null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readSuccessFlag(result: unknown): {
|
|
50
|
+
success?: boolean;
|
|
51
|
+
error?: string;
|
|
52
|
+
} {
|
|
53
|
+
if (!isRecord(result)) return {};
|
|
54
|
+
const record = result;
|
|
55
|
+
return {
|
|
56
|
+
...(typeof record.success === 'boolean' ? { success: record.success } : {}),
|
|
57
|
+
...(typeof record.error === 'string' ? { error: record.error } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class FrontendCallRegistry {
|
|
62
|
+
private readonly pending = new Map<string, Pending>();
|
|
63
|
+
|
|
64
|
+
private key(kind: FrontendCallKind, toolCallId: string): string {
|
|
65
|
+
return `${kind}:${toolCallId}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Park a call until `settle` delivers its result or `timeoutMs` elapses.
|
|
70
|
+
* `signal` (the turn's abort signal) rejects early so an aborted turn does
|
|
71
|
+
* not keep a browser waiting.
|
|
72
|
+
*/
|
|
73
|
+
wait(
|
|
74
|
+
call: Omit<PendingFrontendCall, 'startedAt'>,
|
|
75
|
+
opts: { timeoutMs: number; signal?: AbortSignal },
|
|
76
|
+
): Promise<unknown> {
|
|
77
|
+
const key = this.key(call.kind, call.toolCallId);
|
|
78
|
+
const existing = this.pending.get(key);
|
|
79
|
+
if (existing) {
|
|
80
|
+
this.finish(key);
|
|
81
|
+
existing.reject(
|
|
82
|
+
new Error(`${KIND_LABEL[call.kind]} ${call.toolCallId} was superseded`),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
this.finish(key);
|
|
88
|
+
reject(
|
|
89
|
+
new Error(
|
|
90
|
+
`${KIND_LABEL[call.kind]} timeout after ${opts.timeoutMs}ms: ${call.toolName}`,
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
}, opts.timeoutMs);
|
|
94
|
+
const entry: Pending = {
|
|
95
|
+
...call,
|
|
96
|
+
startedAt: Date.now(),
|
|
97
|
+
resolve,
|
|
98
|
+
reject,
|
|
99
|
+
timer,
|
|
100
|
+
};
|
|
101
|
+
this.pending.set(key, entry);
|
|
102
|
+
if (opts.signal) {
|
|
103
|
+
const onAbort = () => {
|
|
104
|
+
if (this.pending.get(key) !== entry) return;
|
|
105
|
+
this.finish(key);
|
|
106
|
+
reject(
|
|
107
|
+
new Error(`${KIND_LABEL[call.kind]} ${call.toolName} aborted`),
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
if (opts.signal.aborted) onAbort();
|
|
111
|
+
else opts.signal.addEventListener('abort', onAbort, { once: true });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Deliver a result from the socket. Returns false when nothing was waiting
|
|
118
|
+
* for that id (late, duplicate or unknown result — logged by the caller).
|
|
119
|
+
*/
|
|
120
|
+
settle(kind: FrontendCallKind, data: FrontendCallResult): boolean {
|
|
121
|
+
const key = this.key(kind, data.toolCallId);
|
|
122
|
+
const entry = this.pending.get(key);
|
|
123
|
+
if (!entry) return false;
|
|
124
|
+
this.finish(key);
|
|
125
|
+
if (data.error) {
|
|
126
|
+
entry.reject(new Error(data.error));
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (kind === 'agui') {
|
|
130
|
+
const flags = readSuccessFlag(data.result);
|
|
131
|
+
if (flags.success === false) {
|
|
132
|
+
entry.reject(new Error(flags.error || 'Action failed'));
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
entry.resolve(data.result);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Pending calls, oldest first (diagnostics / status). */
|
|
141
|
+
list(): PendingFrontendCall[] {
|
|
142
|
+
return [...this.pending.values()]
|
|
143
|
+
.map(({ kind, toolCallId, toolName, sessionId, startedAt }) => ({
|
|
144
|
+
kind,
|
|
145
|
+
toolCallId,
|
|
146
|
+
toolName,
|
|
147
|
+
sessionId,
|
|
148
|
+
startedAt,
|
|
149
|
+
}))
|
|
150
|
+
.sort((a, b) => a.startedAt - b.startedAt);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
get size(): number {
|
|
154
|
+
return this.pending.size;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private finish(key: string): void {
|
|
158
|
+
const entry = this.pending.get(key);
|
|
159
|
+
if (!entry) return;
|
|
160
|
+
clearTimeout(entry.timer);
|
|
161
|
+
this.pending.delete(key);
|
|
162
|
+
}
|
|
163
|
+
}
|