@ixo/oracle-runtime-workers 0.16.0 → 0.17.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 +3 -0
- package/package.json +5 -3
- package/src/core/budgeted-llm.ts +115 -6
- package/src/core/context-budget.ts +24 -26
- package/src/core/context-window.ts +39 -12
- package/src/core/domain-context/document.ts +100 -0
- package/src/core/domain-context/findings.ts +25 -0
- package/src/core/domain-context/index.ts +28 -0
- package/src/core/domain-context/integrity.ts +60 -0
- package/src/core/domain-context/lru.ts +63 -0
- package/src/core/domain-context/resolver.ts +651 -0
- package/src/core/domain-context/transport.ts +340 -0
- package/src/core/domain-context/turn-context.ts +443 -0
- package/src/core/domain-context/types.ts +119 -0
- package/src/core/domain-context/validator.ts +41 -0
- package/src/core/env.ts +60 -6
- package/src/core/index.ts +2 -0
- package/src/core/llm.ts +225 -4
- package/src/core/main-agent-types.ts +28 -0
- package/src/core/main-agent.ts +61 -4
- package/src/core/middlewares/tool-execution.ts +19 -2
- package/src/core/registries.ts +14 -3
- package/src/core/state.ts +2 -1
- package/src/core/sub-agent-fallback.ts +14 -2
- package/src/core/turn-budget.ts +19 -22
- package/src/do/contracts.ts +23 -2
- package/src/do/domain-context-store.ts +123 -0
- package/src/do/run-request.ts +6 -0
- package/src/do/sse-stream.ts +1 -1
- package/src/do/transcript.ts +9 -0
- package/src/do/turn-body.ts +2 -1
- package/src/do/turn-metadata.ts +13 -6
- package/src/do/user-oracle-do.ts +109 -42
- package/src/index.ts +16 -0
- package/src/llm/byo-adapter.ts +6 -0
- package/src/llm/byo-catalog.ts +7 -4
- package/src/llm/byo-client.ts +42 -7
- package/src/llm/provider-error.ts +21 -3
- package/src/llm/stream-liveness.ts +498 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +6 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The network edge of domain context: the IID anchor lookup on Blocksync and
|
|
3
|
+
* document reads. Reads go only to https origins on an allowlist (configured
|
|
4
|
+
* origins, the VFS, the Matrix homeserver, the IPFS gateway), never follow
|
|
5
|
+
* redirects, never carry URL credentials and never buffer more than the
|
|
6
|
+
* request's byte bound. Private documents are read through the user's VFS
|
|
7
|
+
* UCAN bearer or the host's `readPrivateDocument` adapter.
|
|
8
|
+
*/
|
|
9
|
+
import { CID } from 'multiformats/cid';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import {
|
|
12
|
+
UCAN_STORE_DEFAULT_URLS,
|
|
13
|
+
VFS_DEFAULT_BASE_URLS,
|
|
14
|
+
} from '../../owner-store/ixo-vfs-store';
|
|
15
|
+
import type { RuntimeContext } from '../../plugin-api/types';
|
|
16
|
+
import { vfsBearer } from '../../plugins/vfs/vfs-auth';
|
|
17
|
+
import { INDEX_MAX_BYTES } from './integrity';
|
|
18
|
+
import type {
|
|
19
|
+
DocumentRequest,
|
|
20
|
+
DomainContextOptions,
|
|
21
|
+
DomainTransport,
|
|
22
|
+
} from './types';
|
|
23
|
+
|
|
24
|
+
/** Bound of one document read when the caller's signal does not end it sooner. */
|
|
25
|
+
export const DOCUMENT_READ_TIMEOUT_MS = 10_000;
|
|
26
|
+
|
|
27
|
+
const MATRIX_MEDIA_DOWNLOAD =
|
|
28
|
+
/^\/_matrix\/(media\/v[13]|client\/v1\/media)\/download\//;
|
|
29
|
+
const VFS_FILE_CONTENT = /^\/api\/fs\/files\/[^/]+\/content$/;
|
|
30
|
+
|
|
31
|
+
const IID_QUERY =
|
|
32
|
+
'query DomainAnchor($id: String!) { iids(filter: { id: { equalTo: $id } }) { nodes { id linkedResource } } }';
|
|
33
|
+
|
|
34
|
+
const iidResponse = z.object({
|
|
35
|
+
data: z.object({
|
|
36
|
+
iids: z.object({
|
|
37
|
+
nodes: z.array(z.object({ id: z.string(), linkedResource: z.unknown() })),
|
|
38
|
+
}),
|
|
39
|
+
}),
|
|
40
|
+
errors: z.array(z.unknown()).optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** Longest anchor CID accepted (a CIDv1 sha2-256 in base32 is 59 characters). */
|
|
44
|
+
export const MAX_ANCHOR_CID_CHARS = 128;
|
|
45
|
+
/** Longest anchor `serviceEndpoint` accepted. */
|
|
46
|
+
export const MAX_ANCHOR_URI_CHARS = 2048;
|
|
47
|
+
|
|
48
|
+
/** True when `value` is a parseable CID of a sane length. */
|
|
49
|
+
function isCid(value: string): boolean {
|
|
50
|
+
if (value.length > MAX_ANCHOR_CID_CHARS) return false;
|
|
51
|
+
try {
|
|
52
|
+
CID.parse(value);
|
|
53
|
+
return true;
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The anchor's CID lands in provenance (prompt, stream, SQLite) and its URI
|
|
60
|
+
// in the run's pins, so both are bounded here, before anything copies them.
|
|
61
|
+
const domainResources = z.array(
|
|
62
|
+
z.looseObject({
|
|
63
|
+
id: z.string(),
|
|
64
|
+
proof: z.string().min(1).refine(isCid),
|
|
65
|
+
serviceEndpoint: z.string().min(1).max(MAX_ANCHOR_URI_CHARS),
|
|
66
|
+
encrypted: z.union([z.boolean(), z.string()]).optional(),
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The response body, at most `max` bytes. A declared or streamed size above
|
|
72
|
+
* the bound cancels the body instead of buffering it.
|
|
73
|
+
*/
|
|
74
|
+
export async function boundedBytes(
|
|
75
|
+
response: Response,
|
|
76
|
+
max: number,
|
|
77
|
+
): Promise<Uint8Array> {
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
await response.body?.cancel();
|
|
80
|
+
throw new Error('document-unavailable');
|
|
81
|
+
}
|
|
82
|
+
if (Number(response.headers.get('content-length')) > max) {
|
|
83
|
+
await response.body?.cancel();
|
|
84
|
+
throw new Error('document-too-large');
|
|
85
|
+
}
|
|
86
|
+
const reader = response.body?.getReader();
|
|
87
|
+
if (!reader) return new Uint8Array();
|
|
88
|
+
const chunks: Uint8Array[] = [];
|
|
89
|
+
let size = 0;
|
|
90
|
+
try {
|
|
91
|
+
while (true) {
|
|
92
|
+
const { value, done } = await reader.read();
|
|
93
|
+
if (done) break;
|
|
94
|
+
size += value.length;
|
|
95
|
+
if (size > max) throw new Error('document-too-large');
|
|
96
|
+
chunks.push(value);
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
await reader.cancel();
|
|
100
|
+
reader.releaseLock();
|
|
101
|
+
}
|
|
102
|
+
const out = new Uint8Array(size);
|
|
103
|
+
let offset = 0;
|
|
104
|
+
for (const chunk of chunks) {
|
|
105
|
+
out.set(chunk, offset);
|
|
106
|
+
offset += chunk.length;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function optionalUrl(value: unknown): URL | undefined {
|
|
112
|
+
if (typeof value !== 'string' || !value) return undefined;
|
|
113
|
+
try {
|
|
114
|
+
return new URL(value);
|
|
115
|
+
} catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A configured value, else the per-NETWORK default (as the VFS plugin resolves them). */
|
|
121
|
+
function networkUrl(
|
|
122
|
+
config: RuntimeContext['config'],
|
|
123
|
+
key: string,
|
|
124
|
+
defaults: Record<string, string>,
|
|
125
|
+
): URL | undefined {
|
|
126
|
+
const network = typeof config.NETWORK === 'string' ? config.NETWORK : '';
|
|
127
|
+
return optionalUrl(config[key]) ?? optionalUrl(defaults[network]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function positiveNumber(value: unknown, fallback: number): number {
|
|
131
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
132
|
+
? value
|
|
133
|
+
: fallback;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createDomainTransport(
|
|
137
|
+
options: DomainContextOptions,
|
|
138
|
+
ctx: RuntimeContext,
|
|
139
|
+
): DomainTransport {
|
|
140
|
+
const blocksync =
|
|
141
|
+
typeof ctx.config.BLOCKSYNC_GRAPHQL_URL === 'string'
|
|
142
|
+
? ctx.config.BLOCKSYNC_GRAPHQL_URL
|
|
143
|
+
: '';
|
|
144
|
+
const vfs = networkUrl(ctx.config, 'VFS_BASE_URL', VFS_DEFAULT_BASE_URLS);
|
|
145
|
+
const ucanStore = networkUrl(
|
|
146
|
+
ctx.config,
|
|
147
|
+
'UCAN_STORE_URL',
|
|
148
|
+
UCAN_STORE_DEFAULT_URLS,
|
|
149
|
+
);
|
|
150
|
+
const matrix = optionalUrl(ctx.config.MATRIX_BASE_URL);
|
|
151
|
+
const ipfsGateway = optionalUrl(options.ipfsGateway);
|
|
152
|
+
const allowed = new Set<string>();
|
|
153
|
+
for (const origin of options.allowedOrigins ?? []) {
|
|
154
|
+
const url = optionalUrl(origin);
|
|
155
|
+
if (url) allowed.add(url.origin);
|
|
156
|
+
}
|
|
157
|
+
for (const url of [vfs, matrix, ipfsGateway])
|
|
158
|
+
if (url) allowed.add(url.origin);
|
|
159
|
+
|
|
160
|
+
const isVfsContent = (url: URL) =>
|
|
161
|
+
!!vfs && url.origin === vfs.origin && VFS_FILE_CONTENT.test(url.pathname);
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* `ipfs://<cid>[/path]` on the gateway. The check runs on the URL as parsed
|
|
165
|
+
* (dot segments, encoded ones like `%2e%2e` included, are already resolved
|
|
166
|
+
* there): the path must stay under the gateway's `/ipfs/` and name a CID
|
|
167
|
+
* first. The URI as written may carry no query, fragment or dot segment.
|
|
168
|
+
*/
|
|
169
|
+
const ipfsTarget = (gateway: URL, path: string): URL => {
|
|
170
|
+
const base = gateway.pathname.replace(/\/$/, '');
|
|
171
|
+
let url: URL;
|
|
172
|
+
try {
|
|
173
|
+
url = new URL(`${gateway.origin}${base}/ipfs/${path}`);
|
|
174
|
+
} catch {
|
|
175
|
+
throw new Error('invalid-ipfs-uri');
|
|
176
|
+
}
|
|
177
|
+
const prefix = `${base}/ipfs/`;
|
|
178
|
+
const segments = url.pathname.startsWith(prefix)
|
|
179
|
+
? url.pathname.slice(prefix.length).split('/')
|
|
180
|
+
: [];
|
|
181
|
+
const [root] = segments;
|
|
182
|
+
if (
|
|
183
|
+
!path ||
|
|
184
|
+
path.includes('?') ||
|
|
185
|
+
path.includes('#') ||
|
|
186
|
+
// A dot segment, plain or percent-encoded, anywhere in the URI as
|
|
187
|
+
// written: the parser would resolve it away from the CID it names.
|
|
188
|
+
path.split(/[\\/]/).some((segment) => /^(\.|%2e){1,2}$/i.test(segment)) ||
|
|
189
|
+
url.origin !== gateway.origin ||
|
|
190
|
+
url.search ||
|
|
191
|
+
url.hash ||
|
|
192
|
+
!root ||
|
|
193
|
+
!isCid(root)
|
|
194
|
+
)
|
|
195
|
+
throw new Error('invalid-ipfs-uri');
|
|
196
|
+
return url;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const target = (request: DocumentRequest): URL => {
|
|
200
|
+
const uri = request.uri;
|
|
201
|
+
if (uri.startsWith('ipfs://')) {
|
|
202
|
+
if (!ipfsGateway) throw new Error('ipfs-gateway-unconfigured');
|
|
203
|
+
return ipfsTarget(ipfsGateway, uri.slice('ipfs://'.length));
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
return new URL(uri);
|
|
207
|
+
} catch {
|
|
208
|
+
throw new Error('unsupported-document-uri');
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
isPublic(request) {
|
|
214
|
+
if (request.private) return false;
|
|
215
|
+
try {
|
|
216
|
+
// A VFS read carries the user's bearer even for a "public" anchor.
|
|
217
|
+
return !isVfsContent(target(request));
|
|
218
|
+
} catch {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
async resolve(did, signal) {
|
|
224
|
+
if (!blocksync) throw new Error('iid-unavailable');
|
|
225
|
+
const response = await fetch(blocksync, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
redirect: 'error',
|
|
228
|
+
signal,
|
|
229
|
+
headers: { 'content-type': 'application/json' },
|
|
230
|
+
body: JSON.stringify({ query: IID_QUERY, variables: { id: did } }),
|
|
231
|
+
});
|
|
232
|
+
const body = iidResponse.safeParse(
|
|
233
|
+
JSON.parse(
|
|
234
|
+
new TextDecoder().decode(
|
|
235
|
+
await boundedBytes(response, INDEX_MAX_BYTES),
|
|
236
|
+
),
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
if (!body.success || body.data.errors?.length)
|
|
240
|
+
throw new Error('iid-unavailable');
|
|
241
|
+
const nodes = body.data.data.iids.nodes;
|
|
242
|
+
const node = nodes[0];
|
|
243
|
+
if (nodes.length !== 1 || node?.id !== did)
|
|
244
|
+
throw new Error('iid-unavailable');
|
|
245
|
+
const linked = node.linkedResource ?? [];
|
|
246
|
+
if (!Array.isArray(linked)) throw new Error('invalid-anchor-resources');
|
|
247
|
+
const candidates: unknown[] = linked;
|
|
248
|
+
const matches = domainResources.safeParse(
|
|
249
|
+
candidates.filter(
|
|
250
|
+
(entry) =>
|
|
251
|
+
!!entry &&
|
|
252
|
+
typeof entry === 'object' &&
|
|
253
|
+
'id' in entry &&
|
|
254
|
+
(entry.id === `${did}#dom` || entry.id === '{id}#dom'),
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
if (!matches.success) throw new Error('invalid-anchor-resources');
|
|
258
|
+
const [resource, ...rest] = matches.data;
|
|
259
|
+
if (!resource) return null;
|
|
260
|
+
// Both the full and the shorthand form present: refuse rather than pick one.
|
|
261
|
+
if (rest.length) throw new Error('invalid-anchor-ambiguous');
|
|
262
|
+
return {
|
|
263
|
+
did,
|
|
264
|
+
cid: resource.proof,
|
|
265
|
+
uri: resource.serviceEndpoint,
|
|
266
|
+
private: resource.encrypted === true || resource.encrypted === 'true',
|
|
267
|
+
resolvedAt: Date.now(),
|
|
268
|
+
source: blocksync,
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
async read(request, callerSignal) {
|
|
273
|
+
const signal = callerSignal
|
|
274
|
+
? AbortSignal.any([
|
|
275
|
+
callerSignal,
|
|
276
|
+
AbortSignal.timeout(DOCUMENT_READ_TIMEOUT_MS),
|
|
277
|
+
])
|
|
278
|
+
: AbortSignal.timeout(DOCUMENT_READ_TIMEOUT_MS);
|
|
279
|
+
const url = target(request);
|
|
280
|
+
const vfsContent = isVfsContent(url);
|
|
281
|
+
if (request.private && !vfsContent) {
|
|
282
|
+
if (!options.readPrivateDocument)
|
|
283
|
+
throw new Error('private-reader-unavailable');
|
|
284
|
+
const bytes = await options.readPrivateDocument(request, ctx, signal);
|
|
285
|
+
if (bytes.length > request.maxBytes)
|
|
286
|
+
throw new Error('document-too-large');
|
|
287
|
+
return bytes;
|
|
288
|
+
}
|
|
289
|
+
if (
|
|
290
|
+
url.protocol !== 'https:' ||
|
|
291
|
+
url.username ||
|
|
292
|
+
url.password ||
|
|
293
|
+
!allowed.has(url.origin)
|
|
294
|
+
)
|
|
295
|
+
throw new Error('document-origin-denied');
|
|
296
|
+
const headers: Record<string, string> = {};
|
|
297
|
+
if (vfsContent && vfs) {
|
|
298
|
+
if (!ucanStore) throw new Error('private-reader-unavailable');
|
|
299
|
+
const bearer = await vfsBearer(
|
|
300
|
+
ctx,
|
|
301
|
+
{
|
|
302
|
+
VFS_BASE_URL: vfs.href,
|
|
303
|
+
UCAN_STORE_URL: ucanStore.href,
|
|
304
|
+
VFS_MAX_READ_LINES: positiveNumber(
|
|
305
|
+
ctx.config.VFS_MAX_READ_LINES,
|
|
306
|
+
2000,
|
|
307
|
+
),
|
|
308
|
+
VFS_REQUEST_TIMEOUT_MS: positiveNumber(
|
|
309
|
+
ctx.config.VFS_REQUEST_TIMEOUT_MS,
|
|
310
|
+
DOCUMENT_READ_TIMEOUT_MS,
|
|
311
|
+
),
|
|
312
|
+
},
|
|
313
|
+
'fs/read',
|
|
314
|
+
);
|
|
315
|
+
if ('error' in bearer) throw new Error('document-access-denied');
|
|
316
|
+
headers.Authorization = `Bearer ${bearer.bearer}`;
|
|
317
|
+
} else if (
|
|
318
|
+
!request.private &&
|
|
319
|
+
matrix &&
|
|
320
|
+
url.origin === matrix.origin &&
|
|
321
|
+
MATRIX_MEDIA_DOWNLOAD.test(url.pathname)
|
|
322
|
+
) {
|
|
323
|
+
// Authenticated media: the legacy unauthenticated paths are rewritten
|
|
324
|
+
// to the client v1 endpoint and fetched as the oracle's own device.
|
|
325
|
+
const credentials = await ctx.matrix.botCredentials();
|
|
326
|
+
if (new URL(credentials.baseUrl).origin !== url.origin)
|
|
327
|
+
throw new Error('matrix-origin-mismatch');
|
|
328
|
+
url.pathname = url.pathname.replace(
|
|
329
|
+
/^\/_matrix\/media\/v[13]\/download\//,
|
|
330
|
+
'/_matrix/client/v1/media/download/',
|
|
331
|
+
);
|
|
332
|
+
headers.Authorization = `Bearer ${credentials.accessToken}`;
|
|
333
|
+
}
|
|
334
|
+
return boundedBytes(
|
|
335
|
+
await fetch(url, { headers, redirect: 'error', signal }),
|
|
336
|
+
request.maxBytes,
|
|
337
|
+
);
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|