@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,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opening a document, and the single choke point every write goes through.
|
|
3
|
+
*
|
|
4
|
+
* `applyDocumentEdit` is the only function in this plugin that mutates a
|
|
5
|
+
* document. It enforces, in this order:
|
|
6
|
+
*
|
|
7
|
+
* 1. a live flow is read-only → `read_only_flow`
|
|
8
|
+
* 2. the provider already knows it cannot write → `needs_access`
|
|
9
|
+
* 3. the oracle's power level is below the room's write threshold → `needs_access`
|
|
10
|
+
* 4. the prop allowlist (the caller's `plan` step) → `prop_not_editable` etc.
|
|
11
|
+
* 5. one `doc.transact()` for the whole batch
|
|
12
|
+
* 6. `await provider.waitForFlush()` **before** success is reported
|
|
13
|
+
* 7. a post-flush write check → `needs_access`
|
|
14
|
+
*
|
|
15
|
+
* Steps 6 and 7 exist because matrix-crdt writes are fire-and-forget: a Y.Doc
|
|
16
|
+
* mutation always "succeeds" locally, and a homeserver rejection surfaces only
|
|
17
|
+
* once the batched update is actually sent. Reporting success before the flush
|
|
18
|
+
* is how tools ended up claiming edits that never landed.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { MatrixClient } from 'matrix-js-sdk';
|
|
22
|
+
import type * as Y from 'yjs';
|
|
23
|
+
|
|
24
|
+
import { createConsoleLogger } from '../../core/utils';
|
|
25
|
+
import {
|
|
26
|
+
EDIT_ORIGIN,
|
|
27
|
+
readDocumentTitle,
|
|
28
|
+
seedDocumentTitle,
|
|
29
|
+
} from './document-model';
|
|
30
|
+
import {
|
|
31
|
+
editorError,
|
|
32
|
+
flushTimeout,
|
|
33
|
+
isEditorFailure,
|
|
34
|
+
needsAccess,
|
|
35
|
+
readOnlyFlow,
|
|
36
|
+
type EditorFailure,
|
|
37
|
+
} from './failures';
|
|
38
|
+
import {
|
|
39
|
+
MatrixProviderManager,
|
|
40
|
+
RoomNotAccessibleError,
|
|
41
|
+
type AppConfig,
|
|
42
|
+
} from './provider';
|
|
43
|
+
|
|
44
|
+
const logger = createConsoleLogger({ module: 'editor-content-session' });
|
|
45
|
+
|
|
46
|
+
/** The Matrix event type matrix-crdt writes Y.Doc updates as. */
|
|
47
|
+
const CRDT_UPDATE_EVENT_TYPE = 'matrix-crdt.doc_update';
|
|
48
|
+
|
|
49
|
+
/** Rooms whose canonical alias starts with this hold live flows. */
|
|
50
|
+
const FLOW_ALIAS_PREFIX = '#flow-';
|
|
51
|
+
|
|
52
|
+
/** How long to wait for a write to reach the homeserver before giving up. */
|
|
53
|
+
const FLUSH_TIMEOUT_MS = 20_000;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What the write path needs from the CRDT provider. `MatrixProvider` satisfies
|
|
57
|
+
* it; narrowing keeps the guards testable without a Matrix connection.
|
|
58
|
+
*/
|
|
59
|
+
export interface DocumentWriter {
|
|
60
|
+
readonly canWrite: boolean;
|
|
61
|
+
waitForFlush(): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** What the write path needs from the Matrix client. */
|
|
65
|
+
export interface RoomStateReader {
|
|
66
|
+
getUserId(): string | null;
|
|
67
|
+
getStateEvent(
|
|
68
|
+
roomId: string,
|
|
69
|
+
eventType: string,
|
|
70
|
+
stateKey: string,
|
|
71
|
+
): Promise<Record<string, unknown>>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface DocumentSession {
|
|
75
|
+
doc: Y.Doc;
|
|
76
|
+
provider: DocumentWriter;
|
|
77
|
+
matrixClient: RoomStateReader;
|
|
78
|
+
roomId: string;
|
|
79
|
+
/** Canonical alias, when the homeserver exposes one. */
|
|
80
|
+
alias: string | undefined;
|
|
81
|
+
/** True when the room is a live flow — readable, never writable. */
|
|
82
|
+
isFlow: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function resolveRoomId(
|
|
86
|
+
matrixClient: MatrixClient,
|
|
87
|
+
appConfig: AppConfig,
|
|
88
|
+
): Promise<string> {
|
|
89
|
+
const room = appConfig.matrix.room;
|
|
90
|
+
if (room.type === 'id') return room.value;
|
|
91
|
+
const resolved = await matrixClient.getRoomIdForAlias(room.value);
|
|
92
|
+
return resolved.room_id;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The room's display name from `m.room.name`, used to seed an untitled
|
|
97
|
+
* document. Same reasoning as `resolveAlias`: the editor's Matrix client runs
|
|
98
|
+
* without the sync API, so the client-side room store is not a reliable source
|
|
99
|
+
* of state and the event is read directly.
|
|
100
|
+
*/
|
|
101
|
+
async function readRoomName(
|
|
102
|
+
matrixClient: RoomStateReader,
|
|
103
|
+
roomId: string,
|
|
104
|
+
): Promise<string | undefined> {
|
|
105
|
+
try {
|
|
106
|
+
const state = await matrixClient.getStateEvent(roomId, 'm.room.name', '');
|
|
107
|
+
const name = state?.name;
|
|
108
|
+
return typeof name === 'string' && name.trim().length > 0
|
|
109
|
+
? name
|
|
110
|
+
: undefined;
|
|
111
|
+
} catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function resolveAlias(
|
|
117
|
+
matrixClient: MatrixClient,
|
|
118
|
+
appConfig: AppConfig,
|
|
119
|
+
roomId: string,
|
|
120
|
+
): Promise<string | undefined> {
|
|
121
|
+
if (appConfig.matrix.room.type === 'alias')
|
|
122
|
+
return appConfig.matrix.room.value;
|
|
123
|
+
|
|
124
|
+
// The editor's Matrix client runs without the sync API, so the client-side
|
|
125
|
+
// room store is not a reliable source of state — read the state event.
|
|
126
|
+
try {
|
|
127
|
+
const state: Record<string, unknown> = await matrixClient.getStateEvent(
|
|
128
|
+
roomId,
|
|
129
|
+
'm.room.canonical_alias',
|
|
130
|
+
'',
|
|
131
|
+
);
|
|
132
|
+
const alias = state?.alias;
|
|
133
|
+
return typeof alias === 'string' ? alias : undefined;
|
|
134
|
+
} catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Open the room's Y.Doc, run `work`, and always dispose the provider.
|
|
141
|
+
*
|
|
142
|
+
* Access problems come back as typed failures rather than exceptions, so every
|
|
143
|
+
* tool can return them straight to the agent.
|
|
144
|
+
*/
|
|
145
|
+
export async function withDocument<T>(
|
|
146
|
+
params: { matrixClient: MatrixClient; appConfig: AppConfig },
|
|
147
|
+
work: (session: DocumentSession) => Promise<T>,
|
|
148
|
+
): Promise<T | EditorFailure> {
|
|
149
|
+
const { matrixClient, appConfig } = params;
|
|
150
|
+
|
|
151
|
+
let roomId: string;
|
|
152
|
+
try {
|
|
153
|
+
roomId = await resolveRoomId(matrixClient, appConfig);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
156
|
+
return editorError(`Could not resolve the document's room: ${detail}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const manager = new MatrixProviderManager(matrixClient, appConfig);
|
|
160
|
+
try {
|
|
161
|
+
const { doc, provider } = await manager.init();
|
|
162
|
+
const alias = await resolveAlias(matrixClient, appConfig, roomId);
|
|
163
|
+
return await work({
|
|
164
|
+
doc,
|
|
165
|
+
provider,
|
|
166
|
+
matrixClient,
|
|
167
|
+
roomId,
|
|
168
|
+
alias,
|
|
169
|
+
isFlow: isFlowAlias(alias),
|
|
170
|
+
});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof RoomNotAccessibleError) {
|
|
173
|
+
return needsAccess(roomId, 'this assistant cannot open the document');
|
|
174
|
+
}
|
|
175
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
176
|
+
logger.warn(`Failed to open document ${roomId}: ${detail}`);
|
|
177
|
+
return editorError(`Could not open the document: ${detail}`, roomId);
|
|
178
|
+
} finally {
|
|
179
|
+
await manager.dispose();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** A live flow's room alias is `#flow-…`; a page's is `#page-…`. */
|
|
184
|
+
export function isFlowAlias(alias: string | undefined): boolean {
|
|
185
|
+
return typeof alias === 'string' && alias.startsWith(FLOW_ALIAS_PREFIX);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether the oracle's power level clears the room's threshold for CRDT
|
|
190
|
+
* updates. Collaborative rooms are created with `events_default: 50` and
|
|
191
|
+
* `users_default: 0`, so an un-granted oracle is below the bar and every write
|
|
192
|
+
* it makes is rejected.
|
|
193
|
+
*
|
|
194
|
+
* Fails open: when the power levels cannot be read, the post-flush check is
|
|
195
|
+
* still authoritative.
|
|
196
|
+
*/
|
|
197
|
+
export async function canOracleWrite(
|
|
198
|
+
session: Pick<DocumentSession, 'matrixClient' | 'roomId'>,
|
|
199
|
+
): Promise<boolean> {
|
|
200
|
+
const selfId = session.matrixClient.getUserId();
|
|
201
|
+
if (!selfId) return true;
|
|
202
|
+
|
|
203
|
+
let content: Record<string, unknown>;
|
|
204
|
+
try {
|
|
205
|
+
content = await session.matrixClient.getStateEvent(
|
|
206
|
+
session.roomId,
|
|
207
|
+
'm.room.power_levels',
|
|
208
|
+
'',
|
|
209
|
+
);
|
|
210
|
+
} catch {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
if (!content || typeof content !== 'object') return true;
|
|
214
|
+
|
|
215
|
+
const numberAt = (source: unknown, key: string, fallback: number): number => {
|
|
216
|
+
if (!source || typeof source !== 'object') return fallback;
|
|
217
|
+
const value = Object.getOwnPropertyDescriptor(source, key)?.value;
|
|
218
|
+
return typeof value === 'number' ? value : fallback;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const usersDefault = numberAt(content, 'users_default', 0);
|
|
222
|
+
const selfLevel = numberAt(content.users, selfId, usersDefault);
|
|
223
|
+
const eventsDefault = numberAt(content, 'events_default', 0);
|
|
224
|
+
const required = numberAt(
|
|
225
|
+
content.events,
|
|
226
|
+
CRDT_UPDATE_EVENT_TYPE,
|
|
227
|
+
eventsDefault,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
return selfLevel >= required;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* `canWrite` is a live getter that flips when the homeserver rejects a write,
|
|
235
|
+
* so it is read through a call rather than inline: a direct property read would
|
|
236
|
+
* let the compiler narrow it to its earlier value.
|
|
237
|
+
*/
|
|
238
|
+
function currentlyWritable(writer: DocumentWriter): boolean {
|
|
239
|
+
return writer.canWrite;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function waitForFlushBounded(
|
|
243
|
+
provider: DocumentWriter,
|
|
244
|
+
): Promise<'flushed' | 'timeout' | 'error'> {
|
|
245
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
246
|
+
const timeout = new Promise<'timeout'>((resolve) => {
|
|
247
|
+
timer = setTimeout(() => resolve('timeout'), FLUSH_TIMEOUT_MS);
|
|
248
|
+
});
|
|
249
|
+
try {
|
|
250
|
+
return await Promise.race([
|
|
251
|
+
provider.waitForFlush().then((): 'flushed' => 'flushed'),
|
|
252
|
+
timeout,
|
|
253
|
+
]);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
logger.warn(
|
|
256
|
+
`waitForFlush rejected: ${error instanceof Error ? error.message : String(error)}`,
|
|
257
|
+
);
|
|
258
|
+
return 'error';
|
|
259
|
+
} finally {
|
|
260
|
+
if (timer) clearTimeout(timer);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* A write step: `plan` validates and shapes the edit (no mutation), `apply`
|
|
266
|
+
* performs it. Splitting them is what lets the allowlist run — and refuse —
|
|
267
|
+
* before anything is written.
|
|
268
|
+
*/
|
|
269
|
+
export interface DocumentEditStep<TPlan, TResult> {
|
|
270
|
+
plan: (doc: Y.Doc) => TPlan | EditorFailure;
|
|
271
|
+
apply: (doc: Y.Doc, plan: TPlan) => TResult;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The one write path. Returns the `apply` step's result on success, or a typed
|
|
276
|
+
* failure — never a partially-reported write.
|
|
277
|
+
*/
|
|
278
|
+
export async function applyDocumentEdit<TPlan, TResult>(
|
|
279
|
+
session: DocumentSession,
|
|
280
|
+
step: DocumentEditStep<TPlan, TResult>,
|
|
281
|
+
): Promise<TResult | EditorFailure> {
|
|
282
|
+
if (session.isFlow) {
|
|
283
|
+
return readOnlyFlow(session.roomId, session.alias);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!currentlyWritable(session.provider)) {
|
|
287
|
+
return needsAccess(
|
|
288
|
+
session.roomId,
|
|
289
|
+
'a previous write to this document was rejected',
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!(await canOracleWrite(session))) {
|
|
294
|
+
return needsAccess(
|
|
295
|
+
session.roomId,
|
|
296
|
+
"the assistant's power level in this room is below the write threshold",
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const planned = step.plan(session.doc);
|
|
301
|
+
if (isEditorFailure(planned)) return planned;
|
|
302
|
+
|
|
303
|
+
// Fetched before the transaction because reading room state is async and a
|
|
304
|
+
// Yjs transaction must stay synchronous. Skipped entirely once the document
|
|
305
|
+
// has a title, so the extra round-trip is paid at most once per document.
|
|
306
|
+
const titleSeed = readDocumentTitle(session.doc)
|
|
307
|
+
? undefined
|
|
308
|
+
: await readRoomName(session.matrixClient, session.roomId);
|
|
309
|
+
|
|
310
|
+
let result: TResult | undefined;
|
|
311
|
+
session.doc.transact(() => {
|
|
312
|
+
result = step.apply(session.doc, planned);
|
|
313
|
+
if (titleSeed) seedDocumentTitle(session.doc, titleSeed);
|
|
314
|
+
}, EDIT_ORIGIN);
|
|
315
|
+
|
|
316
|
+
const flush = await waitForFlushBounded(session.provider);
|
|
317
|
+
if (flush !== 'flushed') {
|
|
318
|
+
return flushTimeout(session.roomId);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The homeserver's verdict only arrives with the flush; a rejection here
|
|
322
|
+
// means the mutation exists locally but never reached the room.
|
|
323
|
+
if (!currentlyWritable(session.provider)) {
|
|
324
|
+
return needsAccess(session.roomId, 'the homeserver rejected the write');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (result === undefined) {
|
|
328
|
+
return editorError('The edit produced no result.', session.roomId);
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|