@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.
Files changed (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,58 @@
1
+ import { tool } from '@langchain/core/tools';
2
+ import type { StructuredTool } from 'langchain';
3
+ import { z } from 'zod';
4
+ import { EDITOR_AGENT_TOOL_NAME } from './editor-agent';
5
+ import { GRANT_ACCESS_TOOL } from './failures';
6
+ import type { EditorUnavailableReason } from './prompts';
7
+
8
+ // Mirrors the real sub-agent tool's schema so a model that composed a task for
9
+ // the document assistant still produces a valid call — the answer is just the
10
+ // denial.
11
+ const taskSchema = z.object({
12
+ task: z
13
+ .string()
14
+ .describe('The task you intended to send to the document assistant.'),
15
+ });
16
+
17
+ export interface EditorAccessDeniedToolOptions {
18
+ /** The document room the client reported open. */
19
+ editorRoomId: string;
20
+ /** Why the real document surface refused to bind. */
21
+ reason: EditorUnavailableReason;
22
+ }
23
+
24
+ /**
25
+ * Stand-in for `call_editor_agent` bound when a document is open but the real
26
+ * surface refused to attach (the user is not a verified member of the room, or
27
+ * the sub-agent build failed).
28
+ *
29
+ * Without it, the model is shown a document capability it cannot reach and
30
+ * either narrates its sub-agent task as user-facing text or hunts through
31
+ * `list_capabilities` / `load_capability` for a tool that will never appear.
32
+ */
33
+ export function createEditorAccessDeniedTool(
34
+ options: EditorAccessDeniedToolOptions,
35
+ ): StructuredTool {
36
+ const { editorRoomId, reason } = options;
37
+
38
+ // Membership is resolved with the oracle's admin identity, so a failed check
39
+ // can mean either side is missing — the user OR this oracle (the lookup is
40
+ // forbidden and fails closed). Keep the wording unified.
41
+ const message =
42
+ reason === 'not-member'
43
+ ? `Document unavailable: membership of the document's room (${editorRoomId}) could not be verified — ` +
44
+ `either the user's account or this oracle is not a member, and BOTH must be for document access. ` +
45
+ `Tell the user the document can't be read or edited because either they or this oracle is missing from its room; ` +
46
+ `inviting this oracle to the document (or opening one they own) fixes it. ` +
47
+ `If they only need write access to a document you can already read, the \`${GRANT_ACCESS_TOOL}\` browser tool is what grants it. ` +
48
+ `Do not retry this tool.`
49
+ : `Document unavailable: the document service failed to attach to the room (${editorRoomId}). ` +
50
+ `Tell the user the document assistant is currently unavailable and they can retry shortly. Do not retry this tool.`;
51
+
52
+ return tool(async () => message, {
53
+ name: EDITOR_AGENT_TOOL_NAME,
54
+ description:
55
+ 'Document assistant (unavailable for this request — calls return the denial reason).',
56
+ schema: taskSchema,
57
+ });
58
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Identity of the `call_editor_agent` tool, and the per-room provider config
3
+ * behind it. The tool itself lives in `standalone-editor-tool.ts`: one tool
4
+ * that targets the open document by default and any `room_id` on request.
5
+ */
6
+
7
+ import { computeSubAgentToolName } from '../../core/subagent-as-tool';
8
+ import type { BlocknoteToolsConfig } from './editor-config';
9
+ import type { AppConfig, MatrixRoomConfig } from './provider';
10
+
11
+ /** Display name the tool name is derived from. */
12
+ export const EDITOR_AGENT_NAME = 'Editor Agent';
13
+
14
+ /**
15
+ * The tool name the main agent sees for the document surface. Derived through
16
+ * `computeSubAgentToolName` so it stays stable for the prompt composer, which
17
+ * checks this name before injecting the document-mode prompts: telling the
18
+ * model a document is open without this tool bound makes it narrate its
19
+ * delegation as user-facing text instead of calling anything.
20
+ */
21
+ export const EDITOR_AGENT_TOOL_NAME =
22
+ computeSubAgentToolName(EDITOR_AGENT_NAME);
23
+
24
+ /** Compose the per-room provider config from the plugin's boot-time config. */
25
+ export function buildAppConfig(
26
+ base: BlocknoteToolsConfig,
27
+ room: MatrixRoomConfig,
28
+ ): AppConfig {
29
+ return {
30
+ matrix: { ...base.matrix, room },
31
+ provider: { ...base.provider },
32
+ blocknote: { ...base.blocknote },
33
+ };
34
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Matrix + provider configuration shared by every editor surface (and by the
3
+ * flows plugin, which opens the same Matrix-backed Y.Docs).
4
+ *
5
+ * The three Matrix env vars this reads are owned by the runtime's core base env
6
+ * schema, so the editor plugin declares no `configSchema` of its own — it only
7
+ * extracts typed values out of the already-validated `ctx.config`.
8
+ */
9
+
10
+ import type { MatrixClient } from 'matrix-js-sdk';
11
+
12
+ /** Matrix admin credentials the editor uses to read/write Y.Docs. */
13
+ export interface BlocknoteToolsMatrixConfig {
14
+ baseUrl: string;
15
+ accessToken: string;
16
+ userId: string;
17
+ }
18
+
19
+ /**
20
+ * Build the provider/document config from Matrix credentials. Called once per
21
+ * request by the editor plugin and per call by the flows plugin.
22
+ *
23
+ * `writer.retryIfForbiddenInterval` / `maxForbiddenRetries` are deliberately
24
+ * tight: matrix-crdt defaults to retrying a rejected write every 30s up to 3
25
+ * times, which would keep a tool call open for ~90s before it could report the
26
+ * truth. Room power levels cannot change mid-call, so a rejected write is
27
+ * final — fail fast and let the caller surface `needs_access`.
28
+ */
29
+ export function buildBlocknoteToolsConfig(matrix: BlocknoteToolsMatrixConfig) {
30
+ return {
31
+ matrix: {
32
+ baseUrl: matrix.baseUrl,
33
+ accessToken: matrix.accessToken,
34
+ userId: matrix.userId,
35
+ initialSyncTimeoutMs: 30_000,
36
+ },
37
+ provider: {
38
+ docName: 'document',
39
+ enableAwareness: false,
40
+ retryAttempts: 3,
41
+ retryDelayMs: 5_000,
42
+ flushInterval: 10,
43
+ retryIfForbiddenInterval: 1_000,
44
+ maxForbiddenRetries: 1,
45
+ },
46
+ blocknote: {
47
+ defaultBlockId: undefined,
48
+ blockNamespace: undefined,
49
+ mutableAttributeKeys: [] as string[],
50
+ },
51
+ };
52
+ }
53
+
54
+ /**
55
+ * The editor's config, optionally augmented with a host-provided
56
+ * `MatrixClient`. When `matrixClient` is set, `resolveEditorMatrixClient` uses
57
+ * it directly; when absent, the editor constructs its internal singleton from
58
+ * the Matrix credentials.
59
+ */
60
+ export type BlocknoteToolsConfig = ReturnType<
61
+ typeof buildBlocknoteToolsConfig
62
+ > & {
63
+ matrixClient?: MatrixClient;
64
+ };
@@ -0,0 +1,128 @@
1
+ import { createClient, type MatrixClient } from 'matrix-js-sdk';
2
+ import { createConsoleLogger } from '../../core/utils';
3
+
4
+ /** Matrix admin credentials needed to bootstrap the editor client. */
5
+ export interface EditorMatrixClientConfig {
6
+ baseUrl: string;
7
+ userId: string;
8
+ accessToken: string;
9
+ }
10
+
11
+ const logger = createConsoleLogger({ module: 'editor-mx' });
12
+
13
+ /**
14
+ * Per-isolate singleton wrapper around a `matrix-js-sdk` `MatrixClient`. Used
15
+ * by `@ixo/matrix-crdt` to read/write BlockNote Y.js documents.
16
+ *
17
+ * No `startClient()` / sync is performed — matrix-crdt's `MatrixReader`
18
+ * explicitly tells consumers NOT to use the Sync API and polls the
19
+ * `/events` endpoint itself. No crypto is initialised either
20
+ * (`initRustCrypto` is never called): editor rooms are not E2EE at the doc
21
+ * level in this runtime — matrix-crdt stores updates in its own plain state
22
+ * events — so a crypto-less client is correct, and the crypto WASM stays
23
+ * untouched outside the gateway Durable Object.
24
+ */
25
+ export class EditorMatrixClient {
26
+ private static instance: EditorMatrixClient | null = null;
27
+
28
+ private readonly cfg: EditorMatrixClientConfig;
29
+ private matrixClient: MatrixClient | null = null;
30
+ private isInitialized = false;
31
+ private initializationPromise: Promise<void> | null = null;
32
+
33
+ private constructor(cfg: EditorMatrixClientConfig) {
34
+ this.cfg = cfg;
35
+ }
36
+
37
+ /**
38
+ * Get the singleton instance. First caller supplies the Matrix admin
39
+ * config used for the lifetime of the isolate; subsequent callers
40
+ * receive the same instance and the config argument is ignored.
41
+ */
42
+ public static getInstance(cfg: EditorMatrixClientConfig): EditorMatrixClient {
43
+ if (!EditorMatrixClient.instance) {
44
+ EditorMatrixClient.instance = new EditorMatrixClient(cfg);
45
+ }
46
+ return EditorMatrixClient.instance;
47
+ }
48
+
49
+ /**
50
+ * Test-only — reset the singleton so tests can swap the underlying client.
51
+ */
52
+ public static resetForTesting(): void {
53
+ EditorMatrixClient.instance = null;
54
+ }
55
+
56
+ /**
57
+ * Build the underlying `matrix-js-sdk` client. Idempotent — safe to call
58
+ * concurrently; only the first call does work.
59
+ */
60
+ public async init(): Promise<void> {
61
+ if (this.isInitialized && this.matrixClient) return;
62
+ if (this.initializationPromise) return this.initializationPromise;
63
+
64
+ this.initializationPromise = this.performInitialization();
65
+ try {
66
+ await this.initializationPromise;
67
+ } finally {
68
+ this.initializationPromise = null;
69
+ }
70
+ }
71
+
72
+ private async performInitialization(): Promise<void> {
73
+ const { baseUrl, userId, accessToken } = this.cfg;
74
+ if (!baseUrl || !userId || !accessToken) {
75
+ throw new Error(
76
+ 'Missing Matrix configuration for the editor client (base URL, user id and the bot device token from the gateway).',
77
+ );
78
+ }
79
+
80
+ this.matrixClient = createClient({
81
+ baseUrl,
82
+ accessToken,
83
+ userId,
84
+ timelineSupport: true,
85
+ // Never hand the SDK the bare global: workerd rejects `fetch` invoked
86
+ // with a foreign `this` ("Illegal invocation"), and the SDK calls
87
+ // `this.fetchFn(...)` on its own http-api object.
88
+ fetchFn: (input, init) => fetch(input, init),
89
+ });
90
+ this.isInitialized = true;
91
+ logger.log('EditorMatrixClient ready (no sync, polling-only mode)');
92
+ }
93
+
94
+ public getClient(): MatrixClient {
95
+ if (!this.isInitialized || !this.matrixClient) {
96
+ throw new Error(
97
+ 'EditorMatrixClient not initialized. Call await init() first.',
98
+ );
99
+ }
100
+ return this.matrixClient;
101
+ }
102
+
103
+ public isReady(): boolean {
104
+ return this.isInitialized && this.matrixClient !== null;
105
+ }
106
+
107
+ public async waitUntilReady(): Promise<void> {
108
+ if (this.isReady()) return;
109
+ await this.init();
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Resolve the `MatrixClient` used by editor tools. Prefers the
115
+ * `matrixClient` from the plugin's runtime config (host/test-provided);
116
+ * otherwise lazily constructs the internal singleton.
117
+ *
118
+ * Centralised so every call site (standalone-editor-tool, the flows plugin's
119
+ * flow-doc) goes through one resolution path.
120
+ */
121
+ export async function resolveEditorMatrixClient(
122
+ cfg: EditorMatrixClientConfig & { matrixClient?: MatrixClient },
123
+ ): Promise<MatrixClient> {
124
+ if (cfg.matrixClient) return cfg.matrixClient;
125
+ const inst = EditorMatrixClient.getInstance(cfg);
126
+ await inst.init();
127
+ return inst.getClient();
128
+ }
@@ -0,0 +1,143 @@
1
+ import type { MatrixClient } from 'matrix-js-sdk';
2
+ import { OraclePlugin } from '../../plugin-api/oracle-plugin';
3
+ import type {
4
+ PluginManifest,
5
+ PluginTool,
6
+ RuntimeContext,
7
+ } from '../../plugin-api/types';
8
+ import { EDITOR_AGENT_TOOL_NAME } from './editor-agent';
9
+ import {
10
+ buildBlocknoteToolsConfig,
11
+ type BlocknoteToolsConfig,
12
+ } from './editor-config';
13
+
14
+ // This module must stay statically LIGHT: constructing `new EditorPlugin()`
15
+ // happens at Worker module scope (src/plugins/index.ts), and the tool's
16
+ // module graph (`standalone-editor-tool` → content-tools → provider →
17
+ // @ixo/matrix-crdt → vscode-lib, plus @blocknote/server-util) schedules a
18
+ // timer during module evaluation (vscode-lib's `IdleValue`/`runWhenIdle`),
19
+ // which workerd forbids in global scope. The chain is therefore reached only
20
+ // through `await import()` inside `getRequestTools`, which runs per turn
21
+ // inside a Durable Object request context where timers are allowed.
22
+
23
+ /**
24
+ * Constructor options for `EditorPlugin`. Pass `matrixClient` when the host
25
+ * (or a test) already owns a long-lived matrix-js-sdk client — every editor
26
+ * surface will reuse it instead of constructing the internal singleton.
27
+ */
28
+ export interface EditorPluginOptions {
29
+ matrixClient?: MatrixClient;
30
+ }
31
+
32
+ /**
33
+ * The editor's own polling client needs a device token. It never comes from
34
+ * the env: the gateway keeps a dedicated crypto-less device for plugins and
35
+ * hands its credentials out through `ctx.matrix.botCredentials()`. If that
36
+ * fails the plugin logs and contributes no tools rather than failing the
37
+ * whole request build.
38
+ */
39
+
40
+ const manifest: PluginManifest = {
41
+ title: 'Documents',
42
+ summary:
43
+ "Reads and edits the content of the user's editor documents — prose, " +
44
+ 'structure, and ordering.',
45
+ whenToUse: [
46
+ 'User asks to read, summarize, or explain the document they have open.',
47
+ 'User asks to write, rewrite, shorten, expand, or restructure document content.',
48
+ 'User asks to fix wording, insert a section, reorder sections, or delete content.',
49
+ 'User names a document in their workspace and wants it read or edited.',
50
+ ],
51
+ whenNotToUse: [
52
+ 'Building or configuring flows, action blocks, or forms — this surface edits content only.',
53
+ 'Running or executing anything inside a document.',
54
+ 'IXO entity lookups (use Domain Indexer) — documents are pages, not entities.',
55
+ 'Web search or scraping (use Firecrawl); long-term user memory (use Memory).',
56
+ ],
57
+ examples: [
58
+ {
59
+ user: 'Summarize this page.',
60
+ thought:
61
+ 'Delegate to the document assistant with the open room. It reads the document and returns a summary.',
62
+ tool: EDITOR_AGENT_TOOL_NAME,
63
+ },
64
+ {
65
+ user: 'Rename the "Introduction" heading to "Getting Started".',
66
+ thought:
67
+ 'Content edit — delegate with the exact old and new text so the assistant can locate the block and replace the text.',
68
+ tool: EDITOR_AGENT_TOOL_NAME,
69
+ },
70
+ ],
71
+ tags: ['editor', 'documents', 'pages', 'blocknote', 'content'],
72
+ category: 'data',
73
+ // On-demand so ordinary chats carry no editor Tier-1 entry or gated tools.
74
+ visibility: 'on-demand',
75
+ stability: 'stable',
76
+ };
77
+
78
+ function toolsConfigFor(
79
+ creds: { baseUrl: string; userId: string; accessToken: string },
80
+ matrixClient?: MatrixClient,
81
+ ): BlocknoteToolsConfig {
82
+ return {
83
+ ...buildBlocknoteToolsConfig(creds),
84
+ matrixClient,
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Document content plugin. Contributes one tool, `call_editor_agent`, on every
90
+ * request: it targets the document the user has open (`state.editorRoomId`)
91
+ * by default and any `room_id` the agent names per call.
92
+ *
93
+ * Access is enforced per call inside the tool with `isUserInRoom` (via the
94
+ * gateway Durable Object): the oracle acts with an admin Matrix identity, so
95
+ * "the user's documents" is always computed from the user's membership, never
96
+ * from what the admin can see.
97
+ */
98
+ export class EditorPlugin extends OraclePlugin {
99
+ static readonly NAME = 'editor';
100
+
101
+ readonly name = EditorPlugin.NAME;
102
+
103
+ readonly version = '2.0.0';
104
+
105
+ readonly manifest = manifest;
106
+
107
+ // Intentionally no `configSchema` — the Matrix env vars the editor needs are
108
+ // owned by the core env schema. See `matrixConfigSchema` above.
109
+
110
+ private readonly matrixClient?: MatrixClient;
111
+
112
+ constructor(options: EditorPluginOptions = {}) {
113
+ super();
114
+ this.matrixClient = options.matrixClient;
115
+ }
116
+
117
+ override async getRequestTools(rtCtx: RuntimeContext): Promise<PluginTool[]> {
118
+ // Always bound. One tool covers every case: it targets the open document
119
+ // by default and any `room_id` the agent names, so a page created mid-turn
120
+ // (`create_page_room`) can be written into while another document is open.
121
+ //
122
+ // Not gated on `state.editorRoomId` or `state.spaceId` either — tools are
123
+ // resolved once per request, so a room that only exists after the turn
124
+ // starts is never in state. Access is enforced per call inside the tool
125
+ // (`isUserInRoom(room_id, user)`, fail closed); space membership never
126
+ // implied rights over a document, and the room need not live in that space.
127
+ try {
128
+ const toolsConfig = toolsConfigFor(
129
+ await rtCtx.matrix.botCredentials(),
130
+ this.matrixClient,
131
+ );
132
+ // Lazy on purpose — see the module header. The module registry caches
133
+ // the import, so only the first turn in an isolate pays for it.
134
+ const { createStandaloneEditorTool } =
135
+ await import('./standalone-editor-tool');
136
+ return [createStandaloneEditorTool({ toolsConfig })];
137
+ } catch (error) {
138
+ const detail = error instanceof Error ? error.message : String(error);
139
+ rtCtx.logger.error(`[editor] failed to build standalone tool: ${detail}`);
140
+ return [];
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Typed failures the content tools return as *values*, never as thrown strings.
3
+ *
4
+ * The agent has to be able to branch on the reason: `needs_access` means "ask
5
+ * the user to grant access", `read_only_flow` means "describe, do not edit",
6
+ * `prop_not_editable` means "that prop is off limits — say which".
7
+ */
8
+
9
+ /** The Portal browser tool the user runs to give the assistant write access. */
10
+ export const GRANT_ACCESS_TOOL = 'grant_assistant_access';
11
+
12
+ export type EditorFailureCode =
13
+ | 'needs_access'
14
+ | 'read_only_flow'
15
+ | 'prop_not_editable'
16
+ | 'block_not_found'
17
+ | 'not_a_member'
18
+ | 'no_document'
19
+ | 'flush_timeout'
20
+ | 'error';
21
+
22
+ export interface EditorFailure {
23
+ ok: false;
24
+ code: EditorFailureCode;
25
+ message: string;
26
+ /** The offending prop names, for `prop_not_editable`. */
27
+ props?: string[];
28
+ /** The block the failure is about, when there is one. */
29
+ blockId?: string;
30
+ roomId?: string;
31
+ }
32
+
33
+ export function isEditorFailure(value: unknown): value is EditorFailure {
34
+ return (
35
+ typeof value === 'object' &&
36
+ value !== null &&
37
+ 'ok' in value &&
38
+ value.ok === false &&
39
+ 'code' in value
40
+ );
41
+ }
42
+
43
+ /**
44
+ * The assistant is in the document but cannot write to it. The user has to
45
+ * grant access from the Portal; naming the tool is what lets the agent say
46
+ * exactly what to do.
47
+ */
48
+ export function needsAccess(roomId: string, detail?: string): EditorFailure {
49
+ return {
50
+ ok: false,
51
+ code: 'needs_access',
52
+ roomId,
53
+ message:
54
+ `This assistant does not have permission to write to this document` +
55
+ (detail ? ` (${detail})` : '') +
56
+ `. CALL the \`${GRANT_ACCESS_TOOL}\` browser tool yourself — do not ask ` +
57
+ `the user to run it, and do not tell them you cannot grant yourself ` +
58
+ `access. The tool prompts them to confirm in a dialog; if they confirm, ` +
59
+ `retry the edit once. If it returns \`granted: false\` or a failure, ` +
60
+ `report that plainly. Reads are unaffected.`,
61
+ };
62
+ }
63
+
64
+ /** The document is a live flow: readable, never writable. */
65
+ export function readOnlyFlow(roomId: string, alias?: string): EditorFailure {
66
+ return {
67
+ ok: false,
68
+ code: 'read_only_flow',
69
+ roomId,
70
+ message:
71
+ `This document is a live flow${alias ? ` (${alias})` : ''}. Flows are ` +
72
+ `read-only for this assistant — you can read and describe it, but edits ` +
73
+ `must be made in the flow builder. Do not retry the write.`,
74
+ };
75
+ }
76
+
77
+ /** One or more requested props are outside the block type's allowlist. */
78
+ export function propNotEditable(
79
+ blockId: string,
80
+ blockType: string,
81
+ rejected: Array<{ prop: string; reason: string }>,
82
+ ): EditorFailure {
83
+ const detail = rejected
84
+ .map((entry) => `'${entry.prop}' — ${entry.reason}`)
85
+ .join('; ');
86
+ return {
87
+ ok: false,
88
+ code: 'prop_not_editable',
89
+ blockId,
90
+ props: rejected.map((entry) => entry.prop),
91
+ message:
92
+ `Refused: ${detail}. Nothing was written to block ${blockId} ` +
93
+ `(type '${blockType}'). Tell the user which property could not be ` +
94
+ `changed and why.`,
95
+ };
96
+ }
97
+
98
+ /** The block id is not in the document. */
99
+ export function blockNotFound(blockId: string): EditorFailure {
100
+ return {
101
+ ok: false,
102
+ code: 'block_not_found',
103
+ blockId,
104
+ message:
105
+ `No block with id '${blockId}' exists in this document. Call ` +
106
+ `read_document to get current block ids — never guess or reuse an id ` +
107
+ `from an earlier turn.`,
108
+ };
109
+ }
110
+
111
+ /** The user is not a member of the room, so it is not one of their documents. */
112
+ export function notAMember(roomId: string): EditorFailure {
113
+ return {
114
+ ok: false,
115
+ code: 'not_a_member',
116
+ roomId,
117
+ message:
118
+ `The user is not a member of document room ${roomId}, so it is not one ` +
119
+ `of their documents. Tell them they do not have access to it. Do not retry.`,
120
+ };
121
+ }
122
+
123
+ /** The write was made locally but could not be confirmed as sent. */
124
+ export function flushTimeout(roomId: string): EditorFailure {
125
+ return {
126
+ ok: false,
127
+ code: 'flush_timeout',
128
+ roomId,
129
+ message:
130
+ `The edit could not be confirmed as saved within the time limit — treat ` +
131
+ `it as NOT applied. Tell the user the document service is not responding ` +
132
+ `and they can retry shortly. Do not claim the edit succeeded.`,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Nothing to act on: no document is open and the call named no `room_id`.
138
+ * Distinct from `not_a_member` — there is no room to be a member of yet.
139
+ */
140
+ export function noDocument(): EditorFailure {
141
+ return {
142
+ ok: false,
143
+ code: 'no_document',
144
+ message:
145
+ 'No document is open and no room_id was given. Pass the room id of the ' +
146
+ 'document to work on (find it with list_workspace_pages), or create one ' +
147
+ 'with create_page_room and pass the id it returns.',
148
+ };
149
+ }
150
+
151
+ /** Anything unexpected. Kept distinct so the agent does not misread it as a policy refusal. */
152
+ export function editorError(message: string, roomId?: string): EditorFailure {
153
+ return { ok: false, code: 'error', message, ...(roomId && { roomId }) };
154
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Editor plugin barrel — statically LIGHT by contract.
3
+ *
4
+ * This barrel is re-exported from `src/plugins/index.ts`, which is evaluated
5
+ * at Worker module scope. Nothing here may (transitively) import
6
+ * `@ixo/matrix-crdt` / `@blocknote/server-util` / `@ixo/editor`: vscode-lib
7
+ * (inside matrix-crdt) schedules a timer at module-evaluation time, which
8
+ * workerd forbids in global scope. The heavy tool chain
9
+ * (`standalone-editor-tool`, `content-tools`, `provider`, `blocknote-bridge`,
10
+ * `editor-mx`, `content-session`) is reached only via `await import()` inside
11
+ * `EditorPlugin.getRequestTools` — import those modules directly (not through
12
+ * this barrel) if you need them, and only from a request context.
13
+ */
14
+ export { EditorPlugin, type EditorPluginOptions } from './editor.plugin';
15
+ export { EDITOR_AGENT_NAME, EDITOR_AGENT_TOOL_NAME } from './editor-agent';
16
+ export {
17
+ createEditorAccessDeniedTool,
18
+ type EditorAccessDeniedToolOptions,
19
+ } from './editor-access-denied-tool';
20
+ export {
21
+ buildBlocknoteToolsConfig,
22
+ type BlocknoteToolsConfig,
23
+ type BlocknoteToolsMatrixConfig,
24
+ } from './editor-config';
25
+ export {
26
+ GRANT_ACCESS_TOOL,
27
+ isEditorFailure,
28
+ type EditorFailure,
29
+ type EditorFailureCode,
30
+ } from './failures';
31
+ export { isUserInRoom, invalidateRoomMembership } from './room-membership';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Drop-in replacement for the `jsdom` package on workerd, backed by linkedom.
3
+ *
4
+ * `@blocknote/server-util` does `import * as jsdom from 'jsdom'` and
5
+ * `new jsdom.JSDOM()` purely to get a `window`/`document` pair it can swap
6
+ * onto `globalThis` while BlockNote's markdown/HTML parsing runs. Real jsdom
7
+ * cannot load on workerd (it needs `node:vm` script contexts, which
8
+ * nodejs_compat only stubs), so the module specifier `jsdom` is aliased to
9
+ * this file — in `vitest.config.ts` (`resolve.alias`) and
10
+ * `test/wrangler.test.jsonc` (`alias`) for tests, and deploying hosts must
11
+ * carry the same `alias` entry in their wrangler config.
12
+ *
13
+ * linkedom implements the exact DOM surface the BlockNote parse path touches
14
+ * (`createElement`, `innerHTML`, `querySelectorAll` with `>`/`+` combinators,
15
+ * `insertAdjacentElement`, prosemirror-model's `DOMParser.parse` walk) with
16
+ * one gap: `document.implementation.createHTMLDocument`, which BlockNote's
17
+ * `nestedListsToBlockNoteStructure` uses to build a detached document. The
18
+ * constructor polyfills it with a fresh linkedom document.
19
+ */
20
+ import { parseHTML } from 'linkedom';
21
+
22
+ const EMPTY_PAGE =
23
+ '<!doctype html><html><head><title>jsdom-shim</title></head><body></body></html>';
24
+
25
+ interface DomImplementationLike {
26
+ createHTMLDocument(title?: string): unknown;
27
+ }
28
+
29
+ /**
30
+ * The `jsdom.JSDOM` surface `@blocknote/server-util` consumes: a constructible
31
+ * class exposing `window` (with `document` on it). Nothing else from jsdom's
32
+ * API is provided — BlockNote's server path never touches it.
33
+ */
34
+ export class JSDOM {
35
+ readonly window: ReturnType<typeof parseHTML>;
36
+
37
+ constructor(html: string = EMPTY_PAGE) {
38
+ const { window } = parseHTML(html);
39
+ const doc = window.document;
40
+ if (!doc.implementation) {
41
+ const implementation: DomImplementationLike = {
42
+ createHTMLDocument(title = '') {
43
+ return parseHTML(
44
+ `<!doctype html><html><head><title>${title}</title></head><body></body></html>`,
45
+ ).document;
46
+ },
47
+ };
48
+ Object.defineProperty(doc, 'implementation', {
49
+ value: implementation,
50
+ configurable: true,
51
+ });
52
+ }
53
+ this.window = window;
54
+ }
55
+ }