@ixo/oracle-runtime-workers 0.12.0 → 0.15.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 +32 -10
- package/package.json +14 -9
- package/src/artifacts/config.ts +158 -0
- package/src/artifacts/crypto.ts +94 -0
- package/src/artifacts/routes.ts +73 -0
- package/src/artifacts/store.ts +238 -0
- package/src/artifacts/sweep.ts +200 -0
- package/src/artifacts/tool.ts +103 -0
- package/src/artifacts/viewer-page.ts +614 -0
- package/src/attachments/download.ts +168 -45
- package/src/attachments/index.ts +9 -0
- package/src/attachments/magic.ts +33 -0
- package/src/attachments/pipeline.ts +105 -49
- package/src/attachments/sandbox-archive.ts +9 -8
- package/src/attachments/view-cache.ts +142 -0
- package/src/attachments/view.ts +53 -12
- package/src/channels/auth.ts +267 -0
- package/src/channels/contract.ts +137 -0
- package/src/channels/turns.ts +270 -0
- package/src/core/budgeted-llm.ts +223 -0
- package/src/core/capability-router.ts +273 -0
- package/src/core/context-budget.ts +97 -10
- package/src/core/env.ts +111 -0
- package/src/core/execution-profile.ts +17 -0
- package/src/core/index.ts +267 -9
- package/src/core/llm.ts +30 -9
- package/src/core/main-agent-types.ts +41 -1
- package/src/core/main-agent.ts +367 -65
- package/src/core/manifest.ts +34 -0
- package/src/core/meta-tools.ts +128 -18
- package/src/core/middlewares/capability-gate.ts +130 -12
- package/src/core/middlewares/context-guard.ts +55 -24
- package/src/core/middlewares/index.ts +20 -2
- package/src/core/middlewares/page-context.ts +46 -2
- package/src/core/middlewares/return-direct-first.ts +47 -0
- package/src/core/middlewares/summarization.ts +152 -10
- package/src/core/middlewares/tool-execution.ts +323 -0
- package/src/core/middlewares/tool-repetition-guard.ts +202 -35
- package/src/core/middlewares/turn-boundary.ts +162 -0
- package/src/core/openrouter-pricing.ts +155 -22
- package/src/core/prompt-composer.ts +112 -7
- package/src/core/registries.ts +253 -48
- package/src/core/request-admission.ts +159 -0
- package/src/core/runtime-context.ts +107 -8
- package/src/core/state.ts +1 -1
- package/src/core/sub-agent-fallback.ts +21 -2
- package/src/core/subagent-as-tool.ts +78 -43
- package/src/core/tool-access.ts +189 -0
- package/src/core/tool-scheduler.ts +125 -0
- package/src/core/turn-budget.ts +205 -0
- package/src/core/turn-time-note.ts +43 -0
- package/src/core/user-kv.ts +446 -0
- package/src/core/wrap-plugin-tool.ts +20 -0
- package/src/delivery/plan.ts +278 -0
- package/src/delivery/profile.ts +161 -0
- package/src/delivery/prompt.ts +27 -0
- package/src/delivery/schema.ts +61 -0
- package/src/delivery/shaper.ts +389 -0
- package/src/delivery/types.ts +84 -0
- package/src/do/ambient.ts +51 -15
- package/src/do/contracts.ts +198 -6
- package/src/do/idle-eviction.ts +38 -0
- package/src/do/matrix-turn-ledger.ts +24 -5
- package/src/do/result-store-test-do.ts +137 -3
- package/src/do/result-store.ts +239 -58
- package/src/do/room-mirror.ts +29 -0
- package/src/do/run-buffer.ts +64 -9
- package/src/do/run-coordinator.ts +451 -76
- package/src/do/run-request.ts +2 -0
- package/src/do/run-store-test-do.ts +301 -1
- package/src/do/run-store.ts +385 -43
- package/src/do/secrets-adapter.ts +22 -3
- package/src/do/sse-stream.ts +60 -9
- package/src/do/transcript.ts +48 -5
- package/src/do/turn-body.ts +105 -5
- package/src/do/ucan-service.ts +204 -34
- package/src/do/user-oracle-do.ts +1883 -264
- package/src/feedback/config.ts +127 -0
- package/src/feedback/contract.ts +148 -0
- package/src/feedback/linear-sink.ts +285 -0
- package/src/feedback/privacy.ts +150 -0
- package/src/feedback/reservation.ts +298 -0
- package/src/feedback/submit.ts +247 -0
- package/src/index.ts +138 -6
- package/src/llm/byo-adapter.ts +27 -6
- package/src/llm/byo-catalog.ts +13 -0
- package/src/llm/byo-model-fallback.ts +248 -0
- package/src/llm/byo-service.ts +45 -8
- package/src/llm/provider-error.ts +139 -2
- package/src/matrix/gateway-do.ts +523 -111
- package/src/matrix/group-chat.ts +66 -23
- package/src/matrix/inbox-store.ts +16 -1
- package/src/matrix/ingest.ts +79 -14
- package/src/matrix/media-deadline.ts +130 -0
- package/src/matrix/replay-format.ts +5 -3
- package/src/matrix/reply-parts.ts +92 -0
- package/src/matrix/room-alias.ts +96 -0
- package/src/matrix/user-homeserver.ts +166 -7
- package/src/owner-store/ixo-vfs-store.ts +170 -43
- package/src/owner-store/migrating-store.ts +31 -15
- package/src/owner-store/types.ts +18 -3
- package/src/plugin-api/index.ts +43 -2
- package/src/plugin-api/oracle-plugin.ts +35 -0
- package/src/plugin-api/request-admission.ts +41 -0
- package/src/plugin-api/tool-helper.ts +25 -1
- package/src/plugin-api/tool-plane.ts +92 -0
- package/src/plugin-api/types.ts +194 -6
- package/src/plugins/agui/agui.plugin.ts +31 -16
- package/src/plugins/composio/composio-tools.ts +57 -6
- package/src/plugins/composio/composio-ucan.ts +3 -2
- package/src/plugins/composio/composio.plugin.ts +2 -0
- package/src/plugins/delegated-capabilities.ts +25 -0
- package/src/plugins/editor/content-session.ts +173 -83
- package/src/plugins/editor/content-tools.ts +76 -17
- package/src/plugins/editor/editor-mx.ts +28 -9
- package/src/plugins/editor/editor.plugin.ts +3 -1
- package/src/plugins/editor/failures.ts +28 -7
- package/src/plugins/editor/prompts.ts +2 -1
- package/src/plugins/editor/provider.ts +553 -40
- package/src/plugins/editor/standalone-editor-tool.ts +61 -18
- package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
- package/src/plugins/flows/action-metadata.ts +111 -11
- package/src/plugins/flows/actions.ts +18 -0
- package/src/plugins/flows/decision-context.ts +31 -0
- package/src/plugins/flows/edit.ts +223 -52
- package/src/plugins/flows/errors.ts +22 -1
- package/src/plugins/flows/flow-doc.ts +92 -9
- package/src/plugins/flows/flows.plugin.ts +11 -5
- package/src/plugins/flows/forms.ts +34 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/input-policy.ts +140 -0
- package/src/plugins/flows/prompts.ts +14 -11
- package/src/plugins/flows/read.ts +49 -9
- package/src/plugins/flows/ref-syntax.ts +129 -0
- package/src/plugins/flows/test-homeserver.ts +245 -0
- package/src/plugins/flows/tools/authoring.ts +216 -112
- package/src/plugins/flows/tools/discovery.ts +4 -1
- package/src/plugins/flows/tools/forms.ts +21 -2
- package/src/plugins/flows/tools/settings.ts +34 -13
- package/src/plugins/flows/translator.ts +13 -10
- package/src/plugins/flows/types.ts +167 -42
- package/src/plugins/index.ts +11 -1
- package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
- package/src/plugins/ixo-transaction/index.ts +2 -0
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
- package/src/plugins/ixo-transaction/receipts.ts +99 -0
- package/src/plugins/ixo-transaction/tools.ts +329 -0
- package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
- package/src/plugins/mcp-tool-adapter.ts +23 -2
- package/src/plugins/memory/memory-tools.ts +7 -2
- package/src/plugins/memory/memory-ucan.ts +3 -2
- package/src/plugins/memory/memory.plugin.ts +2 -0
- package/src/plugins/pod-creator/blueprint-store.ts +163 -0
- package/src/plugins/pod-creator/blueprint-types.ts +61 -0
- package/src/plugins/pod-creator/bounded-map.ts +87 -0
- package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
- package/src/plugins/pod-creator/chain-gateway.ts +110 -0
- package/src/plugins/pod-creator/config.ts +85 -0
- package/src/plugins/pod-creator/create-session-store.ts +180 -0
- package/src/plugins/pod-creator/create-tools.ts +538 -0
- package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
- package/src/plugins/pod-creator/index.ts +65 -0
- package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
- package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
- package/src/plugins/pod-creator/stage.ts +201 -0
- package/src/plugins/pod-creator/sub-agents.ts +189 -0
- package/src/plugins/portal/action-log.ts +21 -1
- package/src/plugins/portal/declared-tools.ts +90 -0
- package/src/plugins/portal/portal.plugin.ts +42 -16
- package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
- package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
- package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
- package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
- package/src/plugins/tasks/manifest.ts +1 -1
- package/src/plugins/tasks/middleware.ts +20 -7
- package/src/plugins/tasks/tasks-tools.ts +30 -3
- package/src/plugins/tasks/tasks.plugin.ts +64 -11
- package/src/plugins/user-preferences/schema.ts +12 -1
- package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
- package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
- package/src/plugins/vfs/vfs-client.ts +250 -71
- package/src/plugins/vfs/vfs-content.ts +30 -11
- package/src/plugins/vfs/vfs-errors.ts +22 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
- package/src/plugins/vfs/vfs-tools.ts +60 -20
- package/src/realtime/frontend-call-registry.ts +228 -81
- package/src/realtime/realtime-endpoint.ts +215 -44
- package/src/realtime/session-socket-hub.ts +47 -2
- package/src/secrets/secrets-service.ts +9 -3
- package/src/shell/app.ts +529 -82
- package/src/shell/auth.ts +131 -37
- package/src/shell/turn-body-cap.ts +11 -13
- package/src/sqlite/blob-compactor.ts +61 -28
- package/src/sqlite/database.ts +156 -90
- package/src/sqlite/do-vfs.ts +416 -129
- package/src/sqlite/page-tier.ts +133 -40
- package/src/sqlite/serialization.ts +40 -0
- package/src/sqlite/sessions-store.ts +10 -2
- package/src/sqlite/sqlite-saver.ts +139 -19
- package/src/sqlite/user-kv-store.ts +317 -0
- package/src/tasks/schedule.ts +201 -12
- package/src/tasks/scheduler.ts +586 -98
- package/src/tasks/spec.ts +67 -3
- package/src/tasks/store.ts +292 -27
- package/src/tasks/topic-deliverables.ts +70 -0
package/src/shell/auth.ts
CHANGED
|
@@ -5,9 +5,18 @@
|
|
|
5
5
|
* - Primary: `Authorization: Bearer <invocation>` + `X-Auth-Type: ucan` — a
|
|
6
6
|
* user-self-signed root invocation for `{ can: '*', with: 'ixo:oracle' }`
|
|
7
7
|
* addressed to this oracle's DID. Proves WHO is calling; short TTL.
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* - `x-ucan-delegation: <base64 CAR>` — authorization material for the
|
|
9
|
+
* runtime's downstream calls, used when present. It does NOT
|
|
10
|
+
* authenticate on its own: a delegation is a grant the user hands to
|
|
11
|
+
* this oracle and it travels on as proof in the invocations the runtime
|
|
12
|
+
* mints for other services, so anyone who has seen one could otherwise
|
|
13
|
+
* present it and act as the user. `UCAN_ALLOW_BARE_DELEGATION_AUTH=true`
|
|
14
|
+
* restores the old fallback for clients that do not send an invocation
|
|
15
|
+
* yet (the shell logs a deprecation warning per request). A delegation
|
|
16
|
+
* is only trusted when its issuer equals the authenticated DID and it
|
|
17
|
+
* expires: a delegation with no expiry anywhere in its chain is refused,
|
|
18
|
+
* since the runtime stores it and mints on it long after the request,
|
|
19
|
+
* and nothing else would ever end it.
|
|
11
20
|
*
|
|
12
21
|
* The returned `userDid` is always the cryptographically recovered signer,
|
|
13
22
|
* never a client-claimed value. `did:ixo` keys resolve through Blocksync.
|
|
@@ -18,25 +27,53 @@ import {
|
|
|
18
27
|
createIxoDIDResolver,
|
|
19
28
|
createUCANValidator,
|
|
20
29
|
defineCapability,
|
|
21
|
-
|
|
30
|
+
type DIDKeyResolver,
|
|
31
|
+
type InvocationStore,
|
|
22
32
|
} from '@ixo/ucan';
|
|
23
33
|
|
|
24
34
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
35
|
+
* The shell keeps no replay marks. An auth invocation is a bearer token the
|
|
36
|
+
* client reuses for its whole (short, `UCAN_AUTH_MAX_TTL_SECONDS`-bounded)
|
|
37
|
+
* lifetime, often on several requests at once, so a single-use mark would
|
|
38
|
+
* refuse legitimate repeats. A no-op store also keeps the library from
|
|
39
|
+
* creating its default store, whose hourly `setInterval` would keep a Durable
|
|
40
|
+
* Object resident (this code also runs inside the user object for socket
|
|
41
|
+
* CONNECT auth) and block WebSocket hibernation.
|
|
30
42
|
*/
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
const reusableInvocations: InvocationStore = {
|
|
44
|
+
has: async () => false,
|
|
45
|
+
add: async () => undefined,
|
|
46
|
+
addIfAbsent: async () => true,
|
|
47
|
+
};
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
/**
|
|
50
|
+
* did:ixo key lookups are cached per isolate for this long. A key rotated out
|
|
51
|
+
* of, or removed from, a DID document on-chain is still trusted for up to
|
|
52
|
+
* this long after the change. Failed lookups are never cached.
|
|
53
|
+
*/
|
|
54
|
+
export const IXO_DID_RESOLUTION_CACHE_TTL_MS = 60_000;
|
|
55
|
+
/** Distinct Blocksync URLs that keep a resolver; a deployment uses one. */
|
|
56
|
+
const MAX_IXO_RESOLVERS = 8;
|
|
57
|
+
const ixoResolvers = new Map<string, DIDKeyResolver>();
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The isolate's did:ixo resolver for one Blocksync GraphQL URL, built once and
|
|
61
|
+
* reused so its resolution cache and in-flight de-duplication span requests.
|
|
62
|
+
* Each lookup keeps the library's default timeout.
|
|
63
|
+
*/
|
|
64
|
+
export function sharedIxoDIDResolver(indexerUrl: string): DIDKeyResolver {
|
|
65
|
+
const existing = ixoResolvers.get(indexerUrl);
|
|
66
|
+
if (existing) return existing;
|
|
67
|
+
if (ixoResolvers.size >= MAX_IXO_RESOLVERS) {
|
|
68
|
+
const oldest = ixoResolvers.keys().next().value;
|
|
69
|
+
if (oldest !== undefined) ixoResolvers.delete(oldest);
|
|
70
|
+
}
|
|
71
|
+
const resolver = createIxoDIDResolver({
|
|
72
|
+
indexerUrl,
|
|
73
|
+
cacheTtlMs: IXO_DID_RESOLUTION_CACHE_TTL_MS,
|
|
74
|
+
});
|
|
75
|
+
ixoResolvers.set(indexerUrl, resolver);
|
|
76
|
+
return resolver;
|
|
40
77
|
}
|
|
41
78
|
|
|
42
79
|
export const DEFAULT_UCAN_AUTH_MAX_TTL_SECONDS = 900;
|
|
@@ -48,8 +85,34 @@ export interface AuthConfig {
|
|
|
48
85
|
oracleDid: string;
|
|
49
86
|
blocksyncUri: string;
|
|
50
87
|
maxTtlSeconds?: number;
|
|
88
|
+
/**
|
|
89
|
+
* Accept a bare `x-ucan-delegation` (no invocation) as authentication, the
|
|
90
|
+
* legacy fallback. Off unless `UCAN_ALLOW_BARE_DELEGATION_AUTH=true`.
|
|
91
|
+
*/
|
|
92
|
+
allowBareDelegation?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The auth config of a deployment, from its raw Worker env (shell and user object alike). */
|
|
96
|
+
export function authConfigFromEnv(env: {
|
|
97
|
+
ORACLE_DID: string;
|
|
98
|
+
BLOCKSYNC_GRAPHQL_URL: string;
|
|
99
|
+
UCAN_AUTH_MAX_TTL_SECONDS?: string;
|
|
100
|
+
UCAN_ALLOW_BARE_DELEGATION_AUTH?: string;
|
|
101
|
+
}): AuthConfig {
|
|
102
|
+
return {
|
|
103
|
+
oracleDid: env.ORACLE_DID,
|
|
104
|
+
blocksyncUri: env.BLOCKSYNC_GRAPHQL_URL,
|
|
105
|
+
...(env.UCAN_AUTH_MAX_TTL_SECONDS
|
|
106
|
+
? { maxTtlSeconds: Number(env.UCAN_AUTH_MAX_TTL_SECONDS) }
|
|
107
|
+
: {}),
|
|
108
|
+
allowBareDelegation: env.UCAN_ALLOW_BARE_DELEGATION_AUTH === 'true',
|
|
109
|
+
};
|
|
51
110
|
}
|
|
52
111
|
|
|
112
|
+
/** The 401 a bare delegation gets while the fallback is off. */
|
|
113
|
+
export const INVOCATION_REQUIRED_ERROR =
|
|
114
|
+
'UCAN invocation required: send Authorization: Bearer <invocation> with X-Auth-Type: ucan. An x-ucan-delegation alone no longer authenticates; send it beside the invocation.';
|
|
115
|
+
|
|
53
116
|
export interface AuthResult {
|
|
54
117
|
userDid: string;
|
|
55
118
|
/** Raw delegation CAR when the client supplied a valid one for this user. */
|
|
@@ -91,10 +154,7 @@ class TtlCache<T> {
|
|
|
91
154
|
}
|
|
92
155
|
|
|
93
156
|
const invocationCache = new TtlCache<{ userDid: string; expiration: number }>();
|
|
94
|
-
const delegationCache = new TtlCache<{
|
|
95
|
-
userDid: string;
|
|
96
|
-
expiration?: number;
|
|
97
|
-
}>();
|
|
157
|
+
const delegationCache = new TtlCache<{ userDid: string; expiration: number }>();
|
|
98
158
|
|
|
99
159
|
async function sha256(text: string): Promise<string> {
|
|
100
160
|
const digest = await crypto.subtle.digest(
|
|
@@ -106,23 +166,45 @@ async function sha256(text: string): Promise<string> {
|
|
|
106
166
|
.join('');
|
|
107
167
|
}
|
|
108
168
|
|
|
169
|
+
type InvocationVerdict =
|
|
170
|
+
| { ok: true; userDid: string; expiration: number }
|
|
171
|
+
| { ok: false; error: string };
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Verifications in progress, by token hash. Concurrent requests carrying one
|
|
175
|
+
* invocation (a page load firing several queries with the same token) share
|
|
176
|
+
* one verification; the entry is dropped as soon as it settles, so a failure
|
|
177
|
+
* reaches only the callers that were already waiting on it.
|
|
178
|
+
*/
|
|
179
|
+
const pendingInvocations = new Map<string, Promise<InvocationVerdict>>();
|
|
180
|
+
|
|
109
181
|
async function validateInvocation(
|
|
110
182
|
invocation: string,
|
|
111
183
|
cfg: AuthConfig,
|
|
112
|
-
): Promise<
|
|
113
|
-
| { ok: true; userDid: string; expiration: number }
|
|
114
|
-
| { ok: false; error: string }
|
|
115
|
-
> {
|
|
184
|
+
): Promise<InvocationVerdict> {
|
|
116
185
|
const key = await sha256(invocation);
|
|
117
186
|
const cached = invocationCache.get(key);
|
|
118
187
|
if (cached) return { ok: true, ...cached };
|
|
119
188
|
|
|
120
|
-
|
|
189
|
+
const pending = pendingInvocations.get(key);
|
|
190
|
+
if (pending) return pending;
|
|
191
|
+
const verification = verifyInvocation(invocation, key, cfg).finally(() => {
|
|
192
|
+
pendingInvocations.delete(key);
|
|
193
|
+
});
|
|
194
|
+
pendingInvocations.set(key, verification);
|
|
195
|
+
return verification;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function verifyInvocation(
|
|
199
|
+
invocation: string,
|
|
200
|
+
key: string,
|
|
201
|
+
cfg: AuthConfig,
|
|
202
|
+
): Promise<InvocationVerdict> {
|
|
121
203
|
const validator = await createUCANValidator({
|
|
122
204
|
serverDid: cfg.oracleDid,
|
|
123
205
|
rootIssuers: ['*'],
|
|
124
|
-
didResolver:
|
|
125
|
-
invocationStore,
|
|
206
|
+
didResolver: sharedIxoDIDResolver(cfg.blocksyncUri),
|
|
207
|
+
invocationStore: reusableInvocations,
|
|
126
208
|
});
|
|
127
209
|
const result = await validator.validate(
|
|
128
210
|
invocation,
|
|
@@ -156,23 +238,28 @@ async function validateInvocation(
|
|
|
156
238
|
return { ok: true, ...verdict };
|
|
157
239
|
}
|
|
158
240
|
|
|
159
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Validate a delegation for this oracle: signatures, audience, proof chain,
|
|
243
|
+
* not expired, and a bounded expiry (the earliest across the chain).
|
|
244
|
+
* `userDid` is the delegation's issuer.
|
|
245
|
+
*/
|
|
246
|
+
export async function validateDelegation(
|
|
160
247
|
header: string,
|
|
161
248
|
cfg: AuthConfig,
|
|
162
249
|
): Promise<
|
|
163
|
-
| { ok: true; userDid: string; expiration
|
|
250
|
+
| { ok: true; userDid: string; expiration: number }
|
|
164
251
|
| { ok: false; error: string }
|
|
165
252
|
> {
|
|
166
253
|
const key = await sha256(header);
|
|
167
254
|
const cached = delegationCache.get(key);
|
|
168
255
|
if (cached) return { ok: true, ...cached };
|
|
169
256
|
|
|
170
|
-
sweepInvocationStore();
|
|
171
257
|
const validator = await createUCANValidator({
|
|
172
258
|
serverDid: cfg.oracleDid,
|
|
173
259
|
rootIssuers: [],
|
|
174
|
-
didResolver:
|
|
175
|
-
invocationStore,
|
|
260
|
+
didResolver: sharedIxoDIDResolver(cfg.blocksyncUri),
|
|
261
|
+
invocationStore: reusableInvocations,
|
|
262
|
+
requireExpiration: true,
|
|
176
263
|
});
|
|
177
264
|
const result = await validator.validateDelegation(header);
|
|
178
265
|
if (!result.ok)
|
|
@@ -182,11 +269,13 @@ async function validateDelegation(
|
|
|
182
269
|
};
|
|
183
270
|
if (!result.invoker)
|
|
184
271
|
return { ok: false, error: 'Delegation validated without an invoker DID' };
|
|
272
|
+
// The validator refuses an unbounded chain; this also covers a validator
|
|
273
|
+
// that reported success without the effective expiry.
|
|
274
|
+
if (typeof result.expiration !== 'number' || !isFinite(result.expiration)) {
|
|
275
|
+
return { ok: false, error: 'Delegation must declare an expiration' };
|
|
276
|
+
}
|
|
185
277
|
const verdict = { userDid: result.invoker, expiration: result.expiration };
|
|
186
|
-
const ttlMs =
|
|
187
|
-
typeof result.expiration === 'number'
|
|
188
|
-
? Math.max(1000, result.expiration * 1000 - Date.now())
|
|
189
|
-
: THREE_MINUTES_MS;
|
|
278
|
+
const ttlMs = Math.max(1000, result.expiration * 1000 - Date.now());
|
|
190
279
|
delegationCache.set(key, verdict, Math.min(ttlMs, THREE_MINUTES_MS));
|
|
191
280
|
return { ok: true, ...verdict };
|
|
192
281
|
}
|
|
@@ -216,6 +305,11 @@ export async function authenticate(
|
|
|
216
305
|
'Missing Authorization (UCAN invocation) or x-ucan-delegation header',
|
|
217
306
|
};
|
|
218
307
|
}
|
|
308
|
+
// A delegation proves what the user granted this oracle, not who is
|
|
309
|
+
// calling; it authenticates only through the opt-in legacy fallback.
|
|
310
|
+
if (!invocation && !cfg.allowBareDelegation) {
|
|
311
|
+
return { ok: false, status: 401, error: INVOCATION_REQUIRED_ERROR };
|
|
312
|
+
}
|
|
219
313
|
|
|
220
314
|
let userDid: string | null = null;
|
|
221
315
|
let via: AuthResult['via'] = 'delegation';
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Upper bound on a chat turn's request body (`POST /messages/:sessionId`).
|
|
3
3
|
*
|
|
4
|
-
* The cap bounds the memory one request can pin: the shell
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* The cap bounds the memory one request can pin: the shell enforces it
|
|
5
|
+
* before reading the body (`bodyLimit` — the declared length, or a count of
|
|
6
|
+
* the streamed bytes), so an oversized body is refused with 413 without
|
|
7
|
+
* ever being buffered. It no longer mirrors the Node runtime, which keeps
|
|
8
|
+
* Express body-parser's 100 kb default — a normal Portal turn now exceeds
|
|
9
|
+
* that. Every turn carries the browser-tool catalogue (26–32 tools, 65–70
|
|
10
|
+
* KiB of JSON schema) and the AG-UI action schemas (~27 KiB) before the
|
|
11
|
+
* message text, and a compose-topic turn adds the pinned text in
|
|
10
12
|
* `metadata.contextToolCall`; on devnet turns of 102–115 KiB were refused.
|
|
11
|
-
* 256 KiB leaves room for those. Attachments never count: files go to
|
|
12
|
-
* media first and the body carries only their references.
|
|
13
|
+
* 256 KiB leaves room for those. Attachments never count: files go to
|
|
14
|
+
* Matrix media first and the body carries only their references. The cap
|
|
15
|
+
* counts UTF-8 bytes, as the wire carries them.
|
|
13
16
|
*/
|
|
14
17
|
export const MAX_TURN_BODY_BYTES = 256 * 1024;
|
|
15
|
-
|
|
16
|
-
/** Whether a raw body is over the cap, measured as the UTF-8 bytes the wire carried. */
|
|
17
|
-
export function turnBodyTooLarge(body: string): boolean {
|
|
18
|
-
return new TextEncoder().encode(body).byteLength > MAX_TURN_BODY_BYTES;
|
|
19
|
-
}
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* which is what makes multi-GB users multi-GB — only shrinks if rewritten.
|
|
7
7
|
* `compactStep` rewrites a bounded batch per call so the user object's alarm
|
|
8
8
|
* can chip away at arbitrarily large files without ever hogging a tick;
|
|
9
|
-
* `finishCompaction` reclaims the freed pages once nothing is left.
|
|
9
|
+
* `finishCompaction` reclaims the freed pages once nothing is left. A step
|
|
10
|
+
* is bounded by rows AND by legacy bytes (`COMPACT_STEP_MAX_BYTES`): it
|
|
11
|
+
* reads one blob at a time and holds every page it rewrites until its
|
|
12
|
+
* COMMIT, so a batch of tool outputs with embedded media must not be able
|
|
13
|
+
* to exhaust the isolate's memory.
|
|
10
14
|
*
|
|
11
15
|
* Lossless by construction: each row's blob is read, gzipped, and written
|
|
12
16
|
* back — the read path decompresses transparently, so the decoded value is
|
|
@@ -36,6 +40,12 @@ const TARGETS: readonly CompactTarget[] = [
|
|
|
36
40
|
|
|
37
41
|
const textEncoder = new TextEncoder();
|
|
38
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Legacy bytes one step reads and rewrites at most — except that a step
|
|
45
|
+
* always takes at least one row, however large, so it never stalls.
|
|
46
|
+
*/
|
|
47
|
+
export const COMPACT_STEP_MAX_BYTES = 8 * 1024 * 1024;
|
|
48
|
+
|
|
39
49
|
/** Highest rowid examined per table; persist between calls, `{}` to start. */
|
|
40
50
|
export type CompactCursors = Record<string, number>;
|
|
41
51
|
|
|
@@ -46,6 +56,8 @@ export interface CompactStepResult {
|
|
|
46
56
|
savedBytes: number;
|
|
47
57
|
/** Rows examined (incl. incompressible ones passed over). */
|
|
48
58
|
examined: number;
|
|
59
|
+
/** Legacy bytes of the rows examined. */
|
|
60
|
+
examinedBytes: number;
|
|
49
61
|
/** True once every table's scan is exhausted. */
|
|
50
62
|
done: boolean;
|
|
51
63
|
/** Updated cursors — persist and pass into the next call. */
|
|
@@ -53,73 +65,94 @@ export interface CompactStepResult {
|
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
/**
|
|
56
|
-
* Examine up to `batch` legacy rows and
|
|
57
|
-
*
|
|
68
|
+
* Examine up to `batch` legacy rows — and at most `maxBytes` of them (at
|
|
69
|
+
* least one row) — and rewrite the compressible ones as gzipped blobs.
|
|
70
|
+
* Call repeatedly (persisting `cursors`) until `done`. Where the steps end
|
|
71
|
+
* changes nothing about the result: rows are visited in the same order and
|
|
72
|
+
* each is rewritten exactly as an unbounded run would.
|
|
58
73
|
*/
|
|
59
74
|
export async function compactStep(
|
|
60
75
|
db: DoSqliteDatabase,
|
|
61
76
|
batch = 200,
|
|
62
77
|
cursors: CompactCursors = {},
|
|
78
|
+
opts: { maxBytes?: number } = {},
|
|
63
79
|
): Promise<CompactStepResult> {
|
|
80
|
+
const maxBytes = opts.maxBytes ?? COMPACT_STEP_MAX_BYTES;
|
|
64
81
|
// One transaction per step: the batch lands atomically and cannot
|
|
65
82
|
// interleave with a turn's own checkpoint transaction.
|
|
66
|
-
return db.transaction(() => compactStepInTx(db, batch, cursors));
|
|
83
|
+
return db.transaction(() => compactStepInTx(db, batch, maxBytes, cursors));
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
async function compactStepInTx(
|
|
70
87
|
db: DoSqliteDatabase,
|
|
71
88
|
batch: number,
|
|
89
|
+
maxBytes: number,
|
|
72
90
|
cursors: CompactCursors,
|
|
73
91
|
): Promise<CompactStepResult> {
|
|
74
92
|
let budget = batch;
|
|
75
93
|
let rewritten = 0;
|
|
76
94
|
let savedBytes = 0;
|
|
77
95
|
let examined = 0;
|
|
96
|
+
let examinedBytes = 0;
|
|
97
|
+
let full = false;
|
|
78
98
|
let exhausted = true;
|
|
79
99
|
const next: CompactCursors = { ...cursors };
|
|
80
100
|
|
|
81
101
|
for (const { table, column } of TARGETS) {
|
|
82
102
|
let cursor = next[table] ?? 0;
|
|
83
103
|
while (budget > 0) {
|
|
84
|
-
|
|
104
|
+
// One candidate at a time: the filter loads each value it examines,
|
|
105
|
+
// so a batched query would read every candidate behind the budget.
|
|
106
|
+
const row = await db.get<{ rowid: number; data: Uint8Array | string }>(
|
|
85
107
|
`SELECT rowid, ${column} AS data FROM ${table}
|
|
86
108
|
WHERE rowid > ?
|
|
87
109
|
AND ${column} IS NOT NULL
|
|
88
110
|
AND length(${column}) >= ?
|
|
89
111
|
AND substr(${column}, 1, 2) != X'1F8B'
|
|
90
112
|
ORDER BY rowid
|
|
91
|
-
LIMIT
|
|
92
|
-
[cursor, MIN_COMPRESS_BYTES
|
|
113
|
+
LIMIT 1`,
|
|
114
|
+
[cursor, MIN_COMPRESS_BYTES],
|
|
93
115
|
);
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (isGzip(bytes)) continue;
|
|
102
|
-
const compressed = await gzipBytes(bytes);
|
|
103
|
-
// Incompressible (already-compressed media): leave as-is; the cursor
|
|
104
|
-
// has moved past it, so it is never re-attempted.
|
|
105
|
-
if (compressed.length >= bytes.length) continue;
|
|
106
|
-
await db.run(`UPDATE ${table} SET ${column} = ? WHERE rowid = ?`, [
|
|
107
|
-
compressed,
|
|
108
|
-
rowid,
|
|
109
|
-
]);
|
|
110
|
-
rewritten += 1;
|
|
111
|
-
savedBytes += bytes.length - compressed.length;
|
|
116
|
+
if (row === undefined) break;
|
|
117
|
+
const bytes =
|
|
118
|
+
typeof row.data === 'string' ? textEncoder.encode(row.data) : row.data;
|
|
119
|
+
if (examined > 0 && examinedBytes + bytes.length > maxBytes) {
|
|
120
|
+
// Left for the next step, which starts with it.
|
|
121
|
+
full = true;
|
|
122
|
+
break;
|
|
112
123
|
}
|
|
124
|
+
cursor = Math.max(cursor, row.rowid);
|
|
125
|
+
budget -= 1;
|
|
126
|
+
examined += 1;
|
|
127
|
+
examinedBytes += bytes.length;
|
|
128
|
+
if (isGzip(bytes)) continue;
|
|
129
|
+
const compressed = await gzipBytes(bytes);
|
|
130
|
+
// Incompressible (already-compressed media): leave as-is; the cursor
|
|
131
|
+
// has moved past it, so it is never re-attempted.
|
|
132
|
+
if (compressed.length >= bytes.length) continue;
|
|
133
|
+
await db.run(`UPDATE ${table} SET ${column} = ? WHERE rowid = ?`, [
|
|
134
|
+
compressed,
|
|
135
|
+
row.rowid,
|
|
136
|
+
]);
|
|
137
|
+
rewritten += 1;
|
|
138
|
+
savedBytes += bytes.length - compressed.length;
|
|
113
139
|
}
|
|
114
140
|
next[table] = cursor;
|
|
115
|
-
if (budget <= 0) {
|
|
116
|
-
//
|
|
141
|
+
if (budget <= 0 || full) {
|
|
142
|
+
// A budget ran out mid-table — cannot know whether scans are exhausted.
|
|
117
143
|
exhausted = false;
|
|
118
144
|
break;
|
|
119
145
|
}
|
|
120
146
|
}
|
|
121
147
|
|
|
122
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
rewritten,
|
|
150
|
+
savedBytes,
|
|
151
|
+
examined,
|
|
152
|
+
examinedBytes,
|
|
153
|
+
done: exhausted,
|
|
154
|
+
cursors: next,
|
|
155
|
+
};
|
|
123
156
|
}
|
|
124
157
|
|
|
125
158
|
/**
|