@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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Archive an attachment's original bytes (and, for media, the helper model's
|
|
3
|
+
* analysis) into the user's sandbox at `/workspace/output/<name>` — the Node
|
|
4
|
+
* `FileProcessingService.uploadToSandbox` contract: multipart `file` + `path`
|
|
5
|
+
* to `<SANDBOX_MCP_URL without /mcp>/artifacts/upload`, authenticated with
|
|
6
|
+
* the same `Authorization: Bearer <ucan>` + `X-Auth-Type: ucan` the sandbox
|
|
7
|
+
* MCP client sends.
|
|
8
|
+
*/
|
|
9
|
+
import { sanitizeSandboxPath } from './sanitize';
|
|
10
|
+
|
|
11
|
+
export interface SandboxUploadConfig {
|
|
12
|
+
sandboxMcpUrl: string;
|
|
13
|
+
authHeaders: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SandboxUploadResult {
|
|
17
|
+
path: string;
|
|
18
|
+
url?: string;
|
|
19
|
+
previewUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MAX_ERROR_BODY_LENGTH = 1024;
|
|
23
|
+
|
|
24
|
+
export async function uploadToSandbox(
|
|
25
|
+
bytes: Uint8Array,
|
|
26
|
+
filename: string,
|
|
27
|
+
destPath: string,
|
|
28
|
+
config: SandboxUploadConfig,
|
|
29
|
+
mimetype: string,
|
|
30
|
+
fetchImpl: typeof fetch = fetch,
|
|
31
|
+
): Promise<SandboxUploadResult> {
|
|
32
|
+
const baseUrl = config.sandboxMcpUrl.replace(/\/mcp\/?$/, '');
|
|
33
|
+
const safeFilename = sanitizeSandboxPath(filename);
|
|
34
|
+
const safePath = sanitizeSandboxPath(destPath);
|
|
35
|
+
const formData = new FormData();
|
|
36
|
+
// Copy into a fresh ArrayBuffer-backed view so the Blob never sees a
|
|
37
|
+
// SharedArrayBuffer-typed slice (the DOM lib types reject those).
|
|
38
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
39
|
+
copy.set(bytes);
|
|
40
|
+
formData.set(
|
|
41
|
+
'file',
|
|
42
|
+
new File([copy.buffer], safeFilename, { type: mimetype }),
|
|
43
|
+
);
|
|
44
|
+
formData.set('path', safePath);
|
|
45
|
+
const response = await fetchImpl(`${baseUrl}/artifacts/upload`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { ...config.authHeaders },
|
|
48
|
+
body: formData,
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const errorText = await response.text().catch(() => '');
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Sandbox upload failed (${response.status}): ${errorText.slice(0, MAX_ERROR_BODY_LENGTH)}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const raw: unknown = await response.json();
|
|
57
|
+
const result = raw as { path?: string; url?: string; previewUrl?: string };
|
|
58
|
+
return {
|
|
59
|
+
path: typeof result.path === 'string' ? result.path : safePath,
|
|
60
|
+
...(result.url ? { url: result.url } : {}),
|
|
61
|
+
...(result.previewUrl ? { previewUrl: result.previewUrl } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The `<name>-analysis.md` the extraction lane stores next to media files. */
|
|
66
|
+
export function buildAnalysisMarkdown(
|
|
67
|
+
filename: string,
|
|
68
|
+
mimetype: string,
|
|
69
|
+
sizeBytes: number,
|
|
70
|
+
category: 'image' | 'video' | 'audio',
|
|
71
|
+
content: string,
|
|
72
|
+
): string {
|
|
73
|
+
const labels: Record<'image' | 'video' | 'audio', string> = {
|
|
74
|
+
image: 'Image description',
|
|
75
|
+
video: 'Video description',
|
|
76
|
+
audio: 'Audio transcription',
|
|
77
|
+
};
|
|
78
|
+
return [
|
|
79
|
+
`# ${labels[category]}: ${filename}`,
|
|
80
|
+
'',
|
|
81
|
+
`- **File:** ${filename}`,
|
|
82
|
+
`- **Type:** ${mimetype}`,
|
|
83
|
+
`- **Size:** ${sizeBytes} bytes`,
|
|
84
|
+
`- **Processed:** ${new Date().toISOString()}`,
|
|
85
|
+
'',
|
|
86
|
+
'## Analysis',
|
|
87
|
+
'',
|
|
88
|
+
content,
|
|
89
|
+
].join('\n');
|
|
90
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filename / sandbox-path hygiene — the Node runtime's `attachment-archive.ts`
|
|
3
|
+
* plus `FileProcessingService.sanitizeSandboxPath`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Where every attachment a user sends is archived in their sandbox. */
|
|
7
|
+
export const SANDBOX_OUTPUT_PREFIX = '/workspace/output';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Strip control characters and bracket sequences from a filename to prevent
|
|
11
|
+
* prompt injection when interpolated into LLM context. The archive writes
|
|
12
|
+
* under this exact name.
|
|
13
|
+
*/
|
|
14
|
+
export function sanitizeAttachmentFilename(filename: string): string {
|
|
15
|
+
return (
|
|
16
|
+
filename
|
|
17
|
+
// eslint-disable-next-line no-control-regex
|
|
18
|
+
.replace(/[\x00-\x1f\x7f]/g, '')
|
|
19
|
+
.replace(/[[\]]/g, '')
|
|
20
|
+
.slice(0, 255)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Sanitize a filename/path for the sandbox upload endpoint: only
|
|
26
|
+
* alphanumerics, dots, dashes, underscores and slashes, and no `..` segments
|
|
27
|
+
* so the sandbox cannot be escaped.
|
|
28
|
+
*/
|
|
29
|
+
export function sanitizeSandboxPath(p: string): string {
|
|
30
|
+
const charset = p.replace(/[^a-zA-Z0-9._\-/]/g, '_');
|
|
31
|
+
return charset
|
|
32
|
+
.split('/')
|
|
33
|
+
.filter((seg) => seg !== '..')
|
|
34
|
+
.join('/');
|
|
35
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire shape of a chat attachment — the Node runtime's `AttachmentDto`
|
|
3
|
+
* (`POST /messages/:sessionId` body `attachments[]`, also what
|
|
4
|
+
* `@ixo/oracles-client-sdk` sends): either an `mxcUri` (`mxc://…` or an
|
|
5
|
+
* `http(s)://` URL) or a Matrix `eventId` (encrypted uploads), plus the
|
|
6
|
+
* filename / mimetype / size the client knows.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface AttachmentInput {
|
|
10
|
+
/** `mxc://server/id` or `http(s)://…`. Required when `eventId` is absent. */
|
|
11
|
+
mxcUri?: string;
|
|
12
|
+
/** Matrix event id (`$…`) — encrypted media is fetched and decrypted via the event. */
|
|
13
|
+
eventId?: string;
|
|
14
|
+
filename: string;
|
|
15
|
+
mimetype: string;
|
|
16
|
+
size?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Same cap as the Node DTO (`@ArrayMaxSize`). */
|
|
20
|
+
export const MAX_ATTACHMENTS = 10;
|
|
21
|
+
|
|
22
|
+
const URI_RE = /^(mxc|https?):\/\/.+/i;
|
|
23
|
+
const EVENT_ID_RE = /^\$/;
|
|
24
|
+
const MAX_FILENAME = 255;
|
|
25
|
+
|
|
26
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate the request's `attachments` the way the Node DTO does. Throws an
|
|
32
|
+
* `Error` whose message names the offending entry — callers surface it as
|
|
33
|
+
* the turn's error (Node answers 400 at the controller).
|
|
34
|
+
*/
|
|
35
|
+
export function parseAttachmentInputs(value: unknown): AttachmentInput[] {
|
|
36
|
+
if (value === undefined || value === null) return [];
|
|
37
|
+
if (!Array.isArray(value)) throw new Error('attachments must be an array');
|
|
38
|
+
if (value.length > MAX_ATTACHMENTS)
|
|
39
|
+
throw new Error(`attachments: at most ${MAX_ATTACHMENTS} per message`);
|
|
40
|
+
return value.map((entry, i) => {
|
|
41
|
+
if (!isRecord(entry)) throw new Error(`attachments[${i}]: not an object`);
|
|
42
|
+
const mxcUri = entry['mxcUri'];
|
|
43
|
+
const eventId = entry['eventId'];
|
|
44
|
+
const filename = entry['filename'];
|
|
45
|
+
const mimetype = entry['mimetype'];
|
|
46
|
+
const size = entry['size'];
|
|
47
|
+
if (
|
|
48
|
+
mxcUri !== undefined &&
|
|
49
|
+
(typeof mxcUri !== 'string' || !URI_RE.test(mxcUri))
|
|
50
|
+
)
|
|
51
|
+
throw new Error(
|
|
52
|
+
`attachments[${i}].mxcUri must start with mxc://, http://, or https://`,
|
|
53
|
+
);
|
|
54
|
+
if (
|
|
55
|
+
eventId !== undefined &&
|
|
56
|
+
(typeof eventId !== 'string' || !EVENT_ID_RE.test(eventId))
|
|
57
|
+
)
|
|
58
|
+
throw new Error(`attachments[${i}].eventId must start with $`);
|
|
59
|
+
if (!mxcUri && !eventId)
|
|
60
|
+
throw new Error(
|
|
61
|
+
`attachments[${i}]: either mxcUri or eventId must be provided`,
|
|
62
|
+
);
|
|
63
|
+
if (
|
|
64
|
+
typeof filename !== 'string' ||
|
|
65
|
+
filename.length === 0 ||
|
|
66
|
+
filename.length > MAX_FILENAME
|
|
67
|
+
)
|
|
68
|
+
throw new Error(
|
|
69
|
+
`attachments[${i}].filename is required (≤ ${MAX_FILENAME} chars)`,
|
|
70
|
+
);
|
|
71
|
+
if (typeof mimetype !== 'string' || mimetype.length === 0)
|
|
72
|
+
throw new Error(`attachments[${i}].mimetype is required`);
|
|
73
|
+
if (
|
|
74
|
+
size !== undefined &&
|
|
75
|
+
(typeof size !== 'number' || !Number.isFinite(size) || size < 0)
|
|
76
|
+
)
|
|
77
|
+
throw new Error(`attachments[${i}].size must be a non-negative number`);
|
|
78
|
+
return {
|
|
79
|
+
...(typeof mxcUri === 'string' ? { mxcUri } : {}),
|
|
80
|
+
...(typeof eventId === 'string' ? { eventId } : {}),
|
|
81
|
+
filename,
|
|
82
|
+
mimetype,
|
|
83
|
+
...(typeof size === 'number' ? { size } : {}),
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-fetch one attachment of a session on demand — the `view_attachment`
|
|
3
|
+
* lane behind payload retention (`retention.ts`). The same routing as the
|
|
4
|
+
* turn pipeline applies: when the current model reads the modality the bytes
|
|
5
|
+
* come back as a native content block, otherwise the helper (`vision` role)
|
|
6
|
+
* model turns them into text; plain text is decoded locally.
|
|
7
|
+
*/
|
|
8
|
+
import { classifyAttachment } from './classify';
|
|
9
|
+
import type { NativeAttachment } from './content-blocks';
|
|
10
|
+
import { loadAttachmentBytes, type MatrixMediaSource } from './download';
|
|
11
|
+
import { extractText, type ExtractionProvider } from './extract';
|
|
12
|
+
import { categorizeFile, verifyMagicBytes } from './magic';
|
|
13
|
+
import { routeAttachment } from './route';
|
|
14
|
+
import { sanitizeAttachmentFilename } from './sanitize';
|
|
15
|
+
import type { AttachmentInput } from './types';
|
|
16
|
+
import type { ModelInputCapabilities } from '../core/llm';
|
|
17
|
+
import type { AttachmentMeta } from '../sqlite/serialization';
|
|
18
|
+
import type { Logger } from '../plugin-api/types';
|
|
19
|
+
import { bytesToBase64 } from '../plugins/base64';
|
|
20
|
+
|
|
21
|
+
export type AttachmentView =
|
|
22
|
+
| { kind: 'native'; native: NativeAttachment }
|
|
23
|
+
| { kind: 'text'; text: string };
|
|
24
|
+
|
|
25
|
+
export interface ViewAttachmentDeps {
|
|
26
|
+
source: MatrixMediaSource;
|
|
27
|
+
extraction: ExtractionProvider | null;
|
|
28
|
+
/** Capabilities of the model that will read the result. */
|
|
29
|
+
caps: ModelInputCapabilities;
|
|
30
|
+
roomId?: string;
|
|
31
|
+
fetchImpl?: typeof fetch;
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
logger: Logger;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What the `view_attachment` tool sees on the host: session-scoped access. */
|
|
37
|
+
export interface AttachmentViewSurface {
|
|
38
|
+
/** Whether any message of the session has had its inline payload offloaded. */
|
|
39
|
+
readonly offloaded: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Fetch one of the session's attachments again by the reference the
|
|
42
|
+
* placeholder lists (media event id or mxc URI). Rejects references that do
|
|
43
|
+
* not belong to an attachment of this session.
|
|
44
|
+
*/
|
|
45
|
+
view(ref: string): Promise<{ meta: AttachmentMeta; view: AttachmentView }>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toInput(meta: AttachmentMeta): AttachmentInput {
|
|
49
|
+
return {
|
|
50
|
+
filename: meta.filename,
|
|
51
|
+
mimetype: meta.mimetype,
|
|
52
|
+
...(meta.size !== undefined ? { size: meta.size } : {}),
|
|
53
|
+
...(meta.mxcUri ? { mxcUri: meta.mxcUri } : {}),
|
|
54
|
+
...(meta.eventId ? { eventId: meta.eventId } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function viewAttachment(
|
|
59
|
+
meta: AttachmentMeta,
|
|
60
|
+
deps: ViewAttachmentDeps,
|
|
61
|
+
): Promise<AttachmentView> {
|
|
62
|
+
const attachment = toInput(meta);
|
|
63
|
+
const kind = classifyAttachment(attachment);
|
|
64
|
+
const strategy = routeAttachment(kind, deps.caps);
|
|
65
|
+
const { bytes, mimetype } = await loadAttachmentBytes(
|
|
66
|
+
attachment,
|
|
67
|
+
deps.roomId,
|
|
68
|
+
deps.source,
|
|
69
|
+
{ fetchImpl: deps.fetchImpl, signal: deps.signal },
|
|
70
|
+
);
|
|
71
|
+
deps.logger.log(
|
|
72
|
+
`[attachments] view "${attachment.filename}" (${mimetype}) kind=${kind} → ${strategy} (${bytes.length} bytes)`,
|
|
73
|
+
);
|
|
74
|
+
if (strategy === 'send-native') {
|
|
75
|
+
const loaded = classifyAttachment({
|
|
76
|
+
mimetype,
|
|
77
|
+
filename: attachment.filename,
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
kind: 'native',
|
|
81
|
+
native: {
|
|
82
|
+
kind: loaded === 'image' ? 'image' : 'file',
|
|
83
|
+
mimeType: mimetype,
|
|
84
|
+
base64: bytesToBase64(bytes),
|
|
85
|
+
filename: attachment.filename,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const category = categorizeFile(attachment.mimetype);
|
|
90
|
+
if (category === 'unsupported') {
|
|
91
|
+
return {
|
|
92
|
+
kind: 'text',
|
|
93
|
+
text: `[File "${sanitizeAttachmentFilename(attachment.filename)}" (${attachment.mimetype}) is not a supported file type and could not be processed]`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
verifyMagicBytes(bytes, category, attachment, (m) =>
|
|
97
|
+
deps.logger.warn(`[attachments] ${m}`),
|
|
98
|
+
);
|
|
99
|
+
const { text } = await extractText(
|
|
100
|
+
bytes,
|
|
101
|
+
attachment,
|
|
102
|
+
category,
|
|
103
|
+
deps.extraction,
|
|
104
|
+
{ fetchImpl: deps.fetchImpl, signal: deps.signal },
|
|
105
|
+
);
|
|
106
|
+
return { kind: 'text', text };
|
|
107
|
+
}
|
package/src/core/env.ts
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { OraclePlugin } from '../plugin-api/oracle-plugin';
|
|
3
|
+
import type { Logger } from '../plugin-api/types';
|
|
4
|
+
import { NOOP_LOGGER } from './utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tier-0 (core) environment for the Workers runtime. Mirrors
|
|
8
|
+
* `OracleWorkerEnv` in `src/do/contracts.ts`: every key here is a Worker
|
|
9
|
+
* secret or var. Plugin-owned keys are NOT declared here — each plugin's
|
|
10
|
+
* `configSchema` is folded onto this base by {@link composeEnvSchema}.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately absent versus the Node runtime's base schema: `NODE_ENV`,
|
|
13
|
+
* `PORT`, `SQLITE_DATABASE_PATH`, `MATRIX_STORE_PATH`, `SECP_MNEMONIC`,
|
|
14
|
+
* `RPC_URL`, `OPENAI_API_KEY`, `SESSION_TITLE_MODEL`, `ORACLE_SECRETS`,
|
|
15
|
+
* `LIVE_AGENT_AUTH_API_KEY` — filesystem/process concerns that do not exist
|
|
16
|
+
* on Workers or features this runtime does not carry. Optional features
|
|
17
|
+
* (Nebius, LangSmith, BYO credentials, the account-room secrets key) are
|
|
18
|
+
* read straight from `OracleWorkerEnv` by the code that owns them.
|
|
19
|
+
*
|
|
20
|
+
* Unknown keys are stripped by Zod, so the Durable Object namespaces and any
|
|
21
|
+
* other non-string bindings on the Worker `env` object never reach plugins
|
|
22
|
+
* through `ctx.config`.
|
|
23
|
+
*/
|
|
24
|
+
export const baseEnvSchema = z.object({
|
|
25
|
+
// --- identity -----------------------------------------------------------
|
|
26
|
+
ORACLE_NAME: z.string().min(1),
|
|
27
|
+
/** UCAN audience — the DID users address invocations/delegations to. */
|
|
28
|
+
ORACLE_DID: z.string().min(1),
|
|
29
|
+
/**
|
|
30
|
+
* On-chain entity DID surfaced to the agent as `identity.entityDid`. Falls
|
|
31
|
+
* back to `ORACLE_DID` when unset (see `createRuntimeCore`).
|
|
32
|
+
*/
|
|
33
|
+
ORACLE_ENTITY_DID: z.string().min(1).optional(),
|
|
34
|
+
NETWORK: z.enum(['mainnet', 'testnet', 'devnet']).default('mainnet'),
|
|
35
|
+
|
|
36
|
+
// --- auth ---------------------------------------------------------------
|
|
37
|
+
/** Blocksync GraphQL endpoint used to resolve `did:ixo` verification keys. */
|
|
38
|
+
BLOCKSYNC_GRAPHQL_URL: z.string().min(1),
|
|
39
|
+
/**
|
|
40
|
+
* Max lifetime (seconds) the oracle accepts for a user *auth* invocation.
|
|
41
|
+
* Bounds the replay window server-side regardless of the TTL the client
|
|
42
|
+
* declares. Default 15 minutes.
|
|
43
|
+
*/
|
|
44
|
+
UCAN_AUTH_MAX_TTL_SECONDS: z.coerce.number().int().positive().default(900),
|
|
45
|
+
|
|
46
|
+
// --- matrix -------------------------------------------------------------
|
|
47
|
+
MATRIX_BASE_URL: z.string().min(1),
|
|
48
|
+
MATRIX_ORACLE_ADMIN_USER_ID: z.string().min(1),
|
|
49
|
+
/**
|
|
50
|
+
* The bot account's password. The gateway logs its own device in with it,
|
|
51
|
+
* keeps that device's token in its storage and re-logs in if the token is
|
|
52
|
+
* ever rejected; plugins that need their own client (editor, flows) get a
|
|
53
|
+
* second dedicated device from the gateway. No access token is ever
|
|
54
|
+
* configured: a token is a device, and a device shared with any other
|
|
55
|
+
* client splits its encryption state.
|
|
56
|
+
*/
|
|
57
|
+
MATRIX_ORACLE_ADMIN_PASSWORD: z.string().min(1),
|
|
58
|
+
/** SSSS recovery passphrase — lets a fresh device restore room keys. */
|
|
59
|
+
MATRIX_RECOVERY_PHRASE: z.string().min(1).optional(),
|
|
60
|
+
/** Matrix server name used when composing per-user room aliases. */
|
|
61
|
+
MATRIX_HOMESERVER_NAME: z.string().min(1).optional(),
|
|
62
|
+
|
|
63
|
+
// --- storage ------------------------------------------------------------
|
|
64
|
+
/** Where the user-owned SQLite file lives; unset = the IXO VFS, `matrix` = legacy room media (local harness). */
|
|
65
|
+
OWNER_STORE: z.enum(['matrix', 'vfs']).optional(),
|
|
66
|
+
/** IXO VFS worker base URL when `OWNER_STORE=vfs` (defaults per NETWORK). */
|
|
67
|
+
VFS_BASE_URL: z.string().url().optional(),
|
|
68
|
+
/** UCAN store worker base URL (defaults per NETWORK). */
|
|
69
|
+
UCAN_STORE_URL: z.string().url().optional(),
|
|
70
|
+
|
|
71
|
+
// --- llm ----------------------------------------------------------------
|
|
72
|
+
OPEN_ROUTER_API_KEY: z.string().min(1),
|
|
73
|
+
/**
|
|
74
|
+
* Default model for new chats — an OpenRouter slug. When unset the runtime
|
|
75
|
+
* uses the catalog default. Lets an operator change the default per
|
|
76
|
+
* deployment without a code change.
|
|
77
|
+
*/
|
|
78
|
+
DEFAULT_MODEL: z.string().min(1).optional(),
|
|
79
|
+
/**
|
|
80
|
+
* Multiplier applied to raw provider prices in `GET /models` (the price the
|
|
81
|
+
* user sees). Same default as the Node runtime.
|
|
82
|
+
*/
|
|
83
|
+
MODEL_PRICE_MARKUP: z.coerce.number().positive().optional(),
|
|
84
|
+
/**
|
|
85
|
+
* Extended-thinking effort for the main model. Lower = faster time-to-first
|
|
86
|
+
* token, at some cost to hard multi-step reasoning.
|
|
87
|
+
*/
|
|
88
|
+
MAIN_REASONING_EFFORT: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
89
|
+
|
|
90
|
+
// --- misc ---------------------------------------------------------------
|
|
91
|
+
LOG_LEVEL: z.string().default('info'),
|
|
92
|
+
CORS_ORIGIN: z.string().default('*'),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export type BaseEnv = z.infer<typeof baseEnvSchema>;
|
|
96
|
+
|
|
97
|
+
export interface ComposeEnvSchemaResult {
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
schema: z.ZodObject<any>;
|
|
100
|
+
/**
|
|
101
|
+
* Map of env field name -> the plugin that owns it. Used by
|
|
102
|
+
* {@link validateEnv} to attribute Zod issues to a plugin.
|
|
103
|
+
*/
|
|
104
|
+
pluginOwnership: Map<string, string>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface ValidateEnvError {
|
|
108
|
+
/** Plugin owning the failing field, or `'core'` if from a base schema. */
|
|
109
|
+
plugin: string;
|
|
110
|
+
/** Dotted field path from Zod's issue (e.g. `MEMORY_MCP_URL`). */
|
|
111
|
+
field: string;
|
|
112
|
+
/** Human-readable message from Zod. */
|
|
113
|
+
message: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ValidateEnvResult {
|
|
117
|
+
valid: boolean;
|
|
118
|
+
/** Parsed config object — empty when `valid` is false. */
|
|
119
|
+
config: Record<string, unknown>;
|
|
120
|
+
errors: ValidateEnvError[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fold every plugin's `configSchema` into a single Zod object via
|
|
125
|
+
* `.extend()`. The returned `pluginOwnership` map records which plugin
|
|
126
|
+
* contributed each top-level field so {@link validateEnv} can attribute
|
|
127
|
+
* Zod issues by plugin.
|
|
128
|
+
*
|
|
129
|
+
* Conflict policy: later wins. When two plugins declare the same field,
|
|
130
|
+
* the later definition replaces the earlier one and a warning is emitted
|
|
131
|
+
* via the supplied logger naming both plugins.
|
|
132
|
+
*/
|
|
133
|
+
export function composeEnvSchema(
|
|
134
|
+
plugins: OraclePlugin[],
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
+
baseSchema?: z.ZodObject<any>,
|
|
137
|
+
logger: Logger = NOOP_LOGGER,
|
|
138
|
+
): ComposeEnvSchemaResult {
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
|
+
let merged: z.ZodObject<any> = baseSchema ?? z.object({});
|
|
141
|
+
const pluginOwnership = new Map<string, string>();
|
|
142
|
+
|
|
143
|
+
if (baseSchema) {
|
|
144
|
+
for (const key of Object.keys(baseSchema.shape)) {
|
|
145
|
+
pluginOwnership.set(key, 'core');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const plugin of plugins) {
|
|
150
|
+
if (!plugin.configSchema) continue;
|
|
151
|
+
const shape = plugin.configSchema.shape as Record<string, unknown>;
|
|
152
|
+
for (const key of Object.keys(shape)) {
|
|
153
|
+
const previous = pluginOwnership.get(key);
|
|
154
|
+
if (previous !== undefined && previous !== plugin.name) {
|
|
155
|
+
logger.warn(
|
|
156
|
+
`[boot] env key '${key}' is defined by both '${previous}' and '${plugin.name}'; '${plugin.name}' wins.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
pluginOwnership.set(key, plugin.name);
|
|
160
|
+
}
|
|
161
|
+
merged = merged.extend(shape);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { schema: merged, pluginOwnership };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Validate `env` against the merged schema and produce structured errors
|
|
169
|
+
* naming the owning plugin per failing field.
|
|
170
|
+
*
|
|
171
|
+
* `env` is the Worker's `env` bindings object (or any plain record) — this
|
|
172
|
+
* module never reads `process.env`. On success, `config` holds the parsed
|
|
173
|
+
* object (with any Zod coercions/defaults applied). On failure, `config` is
|
|
174
|
+
* empty and every Zod issue is mapped through `pluginOwnership` to find the
|
|
175
|
+
* owner.
|
|
176
|
+
*/
|
|
177
|
+
export function validateEnv(
|
|
178
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
179
|
+
schema: z.ZodObject<any>,
|
|
180
|
+
env: Record<string, unknown>,
|
|
181
|
+
pluginOwnership: Map<string, string>,
|
|
182
|
+
): ValidateEnvResult {
|
|
183
|
+
const result = schema.safeParse(env);
|
|
184
|
+
if (result.success) {
|
|
185
|
+
return {
|
|
186
|
+
valid: true,
|
|
187
|
+
config: result.data,
|
|
188
|
+
errors: [],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const errors: ValidateEnvError[] = result.error.issues.map((issue) => {
|
|
193
|
+
const topField = issue.path.length > 0 ? String(issue.path[0]) : '<root>';
|
|
194
|
+
const fullField = issue.path.length > 0 ? issue.path.join('.') : '<root>';
|
|
195
|
+
const plugin = pluginOwnership.get(topField) ?? 'unknown';
|
|
196
|
+
return { plugin, field: fullField, message: issue.message };
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return { valid: false, config: {}, errors };
|
|
200
|
+
}
|