@ixo/oracle-runtime-workers 0.24.0 → 0.26.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/package.json +4 -2
- package/src/action-proposals/store.ts +0 -40
- package/src/action-proposals/topic-store.ts +0 -42
- package/src/attachments/extract.ts +108 -14
- package/src/attachments/index.ts +15 -0
- package/src/attachments/pdf-text.ts +99 -0
- package/src/attachments/pipeline.ts +47 -4
- package/src/attachments/sanitize.ts +14 -0
- package/src/attachments/view-cache.ts +95 -0
- package/src/attachments/view.ts +308 -3
- package/src/delivery/plan.ts +4 -1
- package/src/do/contracts.ts +0 -1
- package/src/do/turn-messages.ts +54 -0
- package/src/do/user-oracle-do.ts +309 -143
- package/src/hub-actions/client.ts +58 -5
- package/src/hub-actions/outcome-signature.ts +227 -0
- package/src/hub-actions/request.ts +16 -7
- package/src/hub-actions/store.ts +61 -37
- package/src/hub-actions/transfer.ts +182 -0
- package/src/memory/user-context-invalidation.ts +6 -0
- package/src/plugin-api/types.ts +25 -2
- package/src/plugins/hub-actions/hub-actions.plugin.ts +154 -25
- package/src/plugins/memory/document-chunker.ts +313 -0
- package/src/plugins/memory/document-source.ts +229 -0
- package/src/plugins/memory/memory-engine-client.ts +352 -0
- package/src/plugins/memory/memory.plugin.ts +25 -2
- package/src/plugins/memory/oracle-document-tools.ts +677 -0
- package/src/plugins/vfs/index.ts +11 -2
- package/src/plugins/vfs/vfs-content.ts +86 -22
- package/src/plugins/vfs/vfs.plugin.ts +28 -19
- package/src/shell/app.ts +36 -20
package/src/plugins/vfs/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
resolveVfsConfig,
|
|
3
|
+
VfsPlugin,
|
|
4
|
+
type VfsConfig,
|
|
5
|
+
type VfsPluginOptions,
|
|
6
|
+
} from './vfs.plugin';
|
|
2
7
|
export {
|
|
3
8
|
buildClient,
|
|
4
9
|
createVfsTools,
|
|
@@ -26,7 +31,11 @@ export {
|
|
|
26
31
|
type VfsSearchResult,
|
|
27
32
|
type VfsTreeEntry,
|
|
28
33
|
} from './vfs-client';
|
|
29
|
-
export {
|
|
34
|
+
export {
|
|
35
|
+
isTextMime,
|
|
36
|
+
readForAgent,
|
|
37
|
+
renderBinaryForIngestion,
|
|
38
|
+
} from './vfs-content';
|
|
30
39
|
export {
|
|
31
40
|
isAlreadyExistsConflict,
|
|
32
41
|
isWriteConflict,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { HumanMessage } from '@langchain/core/messages';
|
|
2
2
|
import type { MessageContent } from '@langchain/core/messages';
|
|
3
|
+
import { MAX_MODEL_LANE_BYTES, replyText } from '../../attachments/extract';
|
|
3
4
|
import type { RuntimeContext } from '../../plugin-api/types';
|
|
4
5
|
import { bytesToBase64 } from '../base64';
|
|
5
|
-
import type { VfsClient, VfsFileStat } from './vfs-client';
|
|
6
|
+
import type { VfsClient, VfsContentBytes, VfsFileStat } from './vfs-client';
|
|
6
7
|
import { VfsContentTooLargeError, VfsHttpError } from './vfs-errors';
|
|
7
8
|
|
|
8
9
|
/** Cap on the bytes we download and base64 into the vision model. */
|
|
9
|
-
export const MAX_VISION_BYTES =
|
|
10
|
+
export const MAX_VISION_BYTES = MAX_MODEL_LANE_BYTES;
|
|
10
11
|
|
|
11
12
|
/** MIME types that are text even though they don't start with `text/`. */
|
|
12
13
|
const TEXT_MIME_EXACT = new Set<string>([
|
|
@@ -70,17 +71,37 @@ function isRenderableMime(mime: string): boolean {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* otherwise fetches at most {@link MAX_VISION_BYTES} and routes the bytes
|
|
76
|
-
* through the vision model, returning its description/transcription. A vision
|
|
77
|
-
* failure degrades to a metadata stub.
|
|
74
|
+
* What rendering a binary produced: the model's text (`truncated` when the
|
|
75
|
+
* provider stopped it at its output limit), or a metadata stub.
|
|
78
76
|
*/
|
|
79
|
-
|
|
77
|
+
type BinaryRendering = { text: string; truncated: boolean } | { stub: string };
|
|
78
|
+
|
|
79
|
+
/** True when the provider cut the reply at its output-token limit. */
|
|
80
|
+
function stoppedAtLimit(metadata: unknown): boolean {
|
|
81
|
+
if (typeof metadata !== 'object' || metadata === null) return false;
|
|
82
|
+
const reason =
|
|
83
|
+
'finish_reason' in metadata
|
|
84
|
+
? metadata.finish_reason
|
|
85
|
+
: 'stop_reason' in metadata
|
|
86
|
+
? metadata.stop_reason
|
|
87
|
+
: undefined;
|
|
88
|
+
return reason === 'length' || reason === 'max_tokens';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Shared body of {@link renderBinary} and {@link renderBinaryForIngestion}.
|
|
93
|
+
* Returns a metadata stub without downloading when the stored size or type
|
|
94
|
+
* already rules rendering out; otherwise fetches at most
|
|
95
|
+
* {@link MAX_VISION_BYTES} and routes the bytes through the vision model. A
|
|
96
|
+
* vision failure is a stub too; the caller decides whether a stub is
|
|
97
|
+
* acceptable (`vfs_read`) or an error (ingestion).
|
|
98
|
+
*/
|
|
99
|
+
async function renderBinaryOrStub(
|
|
80
100
|
client: VfsClient,
|
|
81
101
|
rtCtx: RuntimeContext,
|
|
82
102
|
stat: VfsFileStat,
|
|
83
|
-
|
|
103
|
+
preloaded?: VfsContentBytes,
|
|
104
|
+
): Promise<BinaryRendering> {
|
|
84
105
|
const name = basename(stat.path) || stat.id;
|
|
85
106
|
const publicSuffix = stat.publicUrl
|
|
86
107
|
? ` — public link: ${stat.publicUrl}`
|
|
@@ -89,17 +110,23 @@ async function renderBinary(
|
|
|
89
110
|
`[binary file "${name}" — ${mime}, ${size} bytes — too large to render (limit ${MAX_VISION_BYTES} bytes)${publicSuffix}]`;
|
|
90
111
|
const storedMime = stat.mimeType || 'application/octet-stream';
|
|
91
112
|
|
|
92
|
-
if (stat.size > MAX_VISION_BYTES)
|
|
113
|
+
if (stat.size > MAX_VISION_BYTES)
|
|
114
|
+
return { stub: tooLarge(storedMime, stat.size) };
|
|
93
115
|
if (stat.mimeType && !isRenderableMime(stat.mimeType)) {
|
|
94
|
-
return
|
|
116
|
+
return {
|
|
117
|
+
stub: `[binary file "${name}" — ${stat.mimeType}, ${stat.size} bytes — not rendered${publicSuffix}]`,
|
|
118
|
+
};
|
|
95
119
|
}
|
|
96
120
|
|
|
97
|
-
let downloaded:
|
|
121
|
+
let downloaded: VfsContentBytes;
|
|
98
122
|
try {
|
|
99
|
-
downloaded =
|
|
123
|
+
downloaded =
|
|
124
|
+
preloaded ?? (await client.contentBytes(stat.id, MAX_VISION_BYTES));
|
|
100
125
|
} catch (err) {
|
|
101
126
|
if (err instanceof VfsContentTooLargeError) {
|
|
102
|
-
return
|
|
127
|
+
return {
|
|
128
|
+
stub: tooLarge(storedMime, `more than ${MAX_VISION_BYTES}`),
|
|
129
|
+
};
|
|
103
130
|
}
|
|
104
131
|
throw err;
|
|
105
132
|
}
|
|
@@ -109,7 +136,9 @@ async function renderBinary(
|
|
|
109
136
|
const isImage = mime.startsWith('image/');
|
|
110
137
|
const isDoc = isDocMime(mime);
|
|
111
138
|
if (!isImage && !isDoc) {
|
|
112
|
-
return
|
|
139
|
+
return {
|
|
140
|
+
stub: `[binary file "${name}" — ${mime}, ${size} bytes — not rendered${publicSuffix}]`,
|
|
141
|
+
};
|
|
113
142
|
}
|
|
114
143
|
|
|
115
144
|
const base64 = bytesToBase64(new Uint8Array(bytes));
|
|
@@ -138,20 +167,55 @@ async function renderBinary(
|
|
|
138
167
|
const result = await model.invoke([new HumanMessage({ content })], {
|
|
139
168
|
signal: rtCtx.abortSignal,
|
|
140
169
|
});
|
|
141
|
-
const text =
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
170
|
+
const text = replyText(result.content);
|
|
171
|
+
if (text.trim().length > 0) {
|
|
172
|
+
return { text, truncated: stoppedAtLimit(result.response_metadata) };
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
stub: `[${isImage ? 'image' : 'document'} "${name}" — ${mime}, ${size} bytes — no description returned${publicSuffix}]`,
|
|
176
|
+
};
|
|
147
177
|
} catch (err) {
|
|
148
178
|
rtCtx.logger.warn(
|
|
149
179
|
`[vfs] vision render failed for ${name}: ${err instanceof Error ? err.message : String(err)}`,
|
|
150
180
|
);
|
|
151
|
-
return
|
|
181
|
+
return {
|
|
182
|
+
stub: `[${isImage ? 'image' : 'document'} "${name}" — ${mime}, ${size} bytes — could not be rendered${publicSuffix}]`,
|
|
183
|
+
};
|
|
152
184
|
}
|
|
153
185
|
}
|
|
154
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Read a non-text file for the agent: the vision model's description or
|
|
189
|
+
* transcription, or a metadata stub when the file cannot be rendered.
|
|
190
|
+
*/
|
|
191
|
+
async function renderBinary(
|
|
192
|
+
client: VfsClient,
|
|
193
|
+
rtCtx: RuntimeContext,
|
|
194
|
+
stat: VfsFileStat,
|
|
195
|
+
): Promise<string> {
|
|
196
|
+
const rendered = await renderBinaryOrStub(client, rtCtx, stat);
|
|
197
|
+
return 'text' in rendered ? rendered.text : rendered.stub;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Like {@link renderBinary} for callers that need the file's real text (the
|
|
202
|
+
* memory plugin's document ingestion): a file that is too large, not
|
|
203
|
+
* renderable or that the vision model fails on throws instead of returning a
|
|
204
|
+
* stub, so a stub is never mistaken for the document's content. Pass
|
|
205
|
+
* `preloaded` when the bytes are already in hand (no second download);
|
|
206
|
+
* `truncated` reports a reply the provider cut at its output limit.
|
|
207
|
+
*/
|
|
208
|
+
export async function renderBinaryForIngestion(
|
|
209
|
+
client: VfsClient,
|
|
210
|
+
rtCtx: RuntimeContext,
|
|
211
|
+
stat: VfsFileStat,
|
|
212
|
+
preloaded?: VfsContentBytes,
|
|
213
|
+
): Promise<{ text: string; truncated: boolean }> {
|
|
214
|
+
const rendered = await renderBinaryOrStub(client, rtCtx, stat, preloaded);
|
|
215
|
+
if ('text' in rendered) return rendered;
|
|
216
|
+
throw new Error(rendered.stub);
|
|
217
|
+
}
|
|
218
|
+
|
|
155
219
|
/**
|
|
156
220
|
* Resolve a path to a file and return content the agent can read: numbered
|
|
157
221
|
* lines for text, a vision transcription for images/PDFs, or a metadata stub
|
|
@@ -42,6 +42,33 @@ export interface VfsConfig {
|
|
|
42
42
|
VFS_REQUEST_TIMEOUT_MS: number;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Assemble the {@link VfsConfig} from a runtime config: worker URLs derived
|
|
47
|
+
* from `NETWORK` (or the per-deployment overrides), tuning knobs defaulted
|
|
48
|
+
* when absent or invalid. Shared by the plugin and by code that talks to the
|
|
49
|
+
* VFS on its own (the memory plugin's document ingestion).
|
|
50
|
+
*/
|
|
51
|
+
export function resolveVfsConfig(config: unknown): VfsConfig {
|
|
52
|
+
const tuning = configSchema.safeParse(config);
|
|
53
|
+
const siblings = siblingEnvSchema.safeParse(config);
|
|
54
|
+
const network = (siblings.success && siblings.data.NETWORK) || 'mainnet';
|
|
55
|
+
return {
|
|
56
|
+
VFS_BASE_URL:
|
|
57
|
+
(siblings.success && siblings.data.VFS_BASE_URL) ||
|
|
58
|
+
VFS_DEFAULT_BASE_URLS[network] ||
|
|
59
|
+
// Unreachable: `network` is one of the three keys the map declares.
|
|
60
|
+
'https://vfs.ixo.earth',
|
|
61
|
+
UCAN_STORE_URL:
|
|
62
|
+
(siblings.success && siblings.data.UCAN_STORE_URL) ||
|
|
63
|
+
UCAN_STORE_DEFAULT_URLS[network] ||
|
|
64
|
+
'https://store.ucan.ixo.earth',
|
|
65
|
+
VFS_MAX_READ_LINES: tuning.success ? tuning.data.VFS_MAX_READ_LINES : 2000,
|
|
66
|
+
VFS_REQUEST_TIMEOUT_MS: tuning.success
|
|
67
|
+
? tuning.data.VFS_REQUEST_TIMEOUT_MS
|
|
68
|
+
: 20000,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
const manifest: PluginManifest = {
|
|
46
73
|
title: 'Files',
|
|
47
74
|
summary:
|
|
@@ -149,26 +176,8 @@ export class VfsPlugin extends OraclePlugin {
|
|
|
149
176
|
return [];
|
|
150
177
|
}
|
|
151
178
|
|
|
152
|
-
const
|
|
179
|
+
const cfg = resolveVfsConfig(rtCtx.config);
|
|
153
180
|
const siblings = siblingEnvSchema.safeParse(rtCtx.config);
|
|
154
|
-
const network = (siblings.success && siblings.data.NETWORK) || 'mainnet';
|
|
155
|
-
const cfg: VfsConfig = {
|
|
156
|
-
VFS_BASE_URL:
|
|
157
|
-
(siblings.success && siblings.data.VFS_BASE_URL) ||
|
|
158
|
-
VFS_DEFAULT_BASE_URLS[network] ||
|
|
159
|
-
// Unreachable: `network` is one of the three keys the map declares.
|
|
160
|
-
'https://vfs.ixo.earth',
|
|
161
|
-
UCAN_STORE_URL:
|
|
162
|
-
(siblings.success && siblings.data.UCAN_STORE_URL) ||
|
|
163
|
-
UCAN_STORE_DEFAULT_URLS[network] ||
|
|
164
|
-
'https://store.ucan.ixo.earth',
|
|
165
|
-
VFS_MAX_READ_LINES: tuning.success
|
|
166
|
-
? tuning.data.VFS_MAX_READ_LINES
|
|
167
|
-
: 2000,
|
|
168
|
-
VFS_REQUEST_TIMEOUT_MS: tuning.success
|
|
169
|
-
? tuning.data.VFS_REQUEST_TIMEOUT_MS
|
|
170
|
-
: 20000,
|
|
171
|
-
};
|
|
172
181
|
|
|
173
182
|
const tools = createVfsTools({
|
|
174
183
|
cfg,
|
package/src/shell/app.ts
CHANGED
|
@@ -78,22 +78,7 @@ import {
|
|
|
78
78
|
} from '../feedback/submit';
|
|
79
79
|
import { z } from 'zod';
|
|
80
80
|
import { parseOutcome } from '@ixo/action-protocol';
|
|
81
|
-
|
|
82
|
-
async function equalOutcomeKeys(
|
|
83
|
-
provided: string | null,
|
|
84
|
-
expected: string | undefined,
|
|
85
|
-
): Promise<boolean> {
|
|
86
|
-
if (!provided || !expected || expected.length < 32 || provided.length > 512)
|
|
87
|
-
return false;
|
|
88
|
-
const digest = async (value: string) =>
|
|
89
|
-
new Uint8Array(
|
|
90
|
-
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)),
|
|
91
|
-
);
|
|
92
|
-
const [a, b] = await Promise.all([digest(provided), digest(expected)]);
|
|
93
|
-
let difference = 0;
|
|
94
|
-
for (let i = 0; i < a.length; i++) difference |= (a[i] ?? 0) ^ (b[i] ?? 0);
|
|
95
|
-
return difference === 0;
|
|
96
|
-
}
|
|
81
|
+
import { verifyHubOutcomeCallback } from '../hub-actions/outcome-signature';
|
|
97
82
|
|
|
98
83
|
const DebugEventBody = z.object({
|
|
99
84
|
type: z
|
|
@@ -217,6 +202,22 @@ export const BYO_LLM_BODY_BYTES = 64 * 1024;
|
|
|
217
202
|
export const DEBUG_BODY_BYTES = 64 * 1024;
|
|
218
203
|
|
|
219
204
|
/** A body cap enforced before the body is buffered: over it, 413 and the handler never runs. */
|
|
205
|
+
/**
|
|
206
|
+
* The audience a signed Hub outcome callback must be signed for: the origin
|
|
207
|
+
* of `ORACLE_PUBLIC_URL` when it is set, else the origin of the request URL.
|
|
208
|
+
* An unparsable public URL yields '' (which never verifies).
|
|
209
|
+
*/
|
|
210
|
+
export function callbackAudience(
|
|
211
|
+
publicUrl: string | undefined,
|
|
212
|
+
requestUrl: string,
|
|
213
|
+
): string {
|
|
214
|
+
try {
|
|
215
|
+
return new URL(publicUrl || requestUrl).origin;
|
|
216
|
+
} catch {
|
|
217
|
+
return '';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
220
221
|
function limitBody(maxSize: number) {
|
|
221
222
|
return bodyLimit({
|
|
222
223
|
maxSize,
|
|
@@ -455,17 +456,32 @@ export function createShell(
|
|
|
455
456
|
});
|
|
456
457
|
|
|
457
458
|
app.use('/api/internal/actions/outcomes', limitBody(64 * 1024));
|
|
459
|
+
// Auth Hub's signed outcome callbacks: on whenever Hub actions are
|
|
460
|
+
// configured. The signature is checked against the Hub's own did:web key
|
|
461
|
+
// (from AUTH_HUB_URL) for this oracle's origin, then the owner object
|
|
462
|
+
// confirms the outcome through Auth Hub's authenticated status API.
|
|
458
463
|
app.post('/api/internal/actions/outcomes', async (c) => {
|
|
459
464
|
if (
|
|
460
465
|
c.env.HUB_ACTIONS_ENABLED !== 'true' ||
|
|
461
|
-
!
|
|
462
|
-
|
|
463
|
-
c.env.AUTH_HUB_ACTION_OUTCOME_KEY,
|
|
464
|
-
))
|
|
466
|
+
!c.env.AUTH_HUB_URL ||
|
|
467
|
+
!c.env.AUTH_HUB_CHANNEL_SERVICE_KEY
|
|
465
468
|
)
|
|
466
469
|
return c.json({ code: 'unauthorized' }, 401);
|
|
467
470
|
const body = await c.req.raw.text();
|
|
468
471
|
if (body.length > 64_000) return c.json({ code: 'request_too_large' }, 413);
|
|
472
|
+
if (
|
|
473
|
+
!(await verifyHubOutcomeCallback({
|
|
474
|
+
hubUrl: c.env.AUTH_HUB_URL,
|
|
475
|
+
rawBody: body,
|
|
476
|
+
headers: c.req.raw.headers,
|
|
477
|
+
// The origin the Hub signs for: the configured public URL when
|
|
478
|
+
// set (a Worker reachable under several hostnames), else the
|
|
479
|
+
// origin this request came in on.
|
|
480
|
+
audience: callbackAudience(c.env.ORACLE_PUBLIC_URL, c.req.url),
|
|
481
|
+
...(c.env.AUTH_HUB ? { fetcher: c.env.AUTH_HUB } : {}),
|
|
482
|
+
}))
|
|
483
|
+
)
|
|
484
|
+
return c.json({ code: 'unauthorized' }, 401);
|
|
469
485
|
try {
|
|
470
486
|
const outcome = parseOutcome(JSON.parse(body));
|
|
471
487
|
await userStub(c.env, outcome.ownerDid).recordHubActionOutcome(outcome);
|