@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.
Files changed (205) hide show
  1. package/README.md +32 -10
  2. package/package.json +14 -9
  3. package/src/artifacts/config.ts +158 -0
  4. package/src/artifacts/crypto.ts +94 -0
  5. package/src/artifacts/routes.ts +73 -0
  6. package/src/artifacts/store.ts +238 -0
  7. package/src/artifacts/sweep.ts +200 -0
  8. package/src/artifacts/tool.ts +103 -0
  9. package/src/artifacts/viewer-page.ts +614 -0
  10. package/src/attachments/download.ts +168 -45
  11. package/src/attachments/index.ts +9 -0
  12. package/src/attachments/magic.ts +33 -0
  13. package/src/attachments/pipeline.ts +105 -49
  14. package/src/attachments/sandbox-archive.ts +9 -8
  15. package/src/attachments/view-cache.ts +142 -0
  16. package/src/attachments/view.ts +53 -12
  17. package/src/channels/auth.ts +267 -0
  18. package/src/channels/contract.ts +137 -0
  19. package/src/channels/turns.ts +270 -0
  20. package/src/core/budgeted-llm.ts +223 -0
  21. package/src/core/capability-router.ts +273 -0
  22. package/src/core/context-budget.ts +97 -10
  23. package/src/core/env.ts +111 -0
  24. package/src/core/execution-profile.ts +17 -0
  25. package/src/core/index.ts +267 -9
  26. package/src/core/llm.ts +30 -9
  27. package/src/core/main-agent-types.ts +41 -1
  28. package/src/core/main-agent.ts +367 -65
  29. package/src/core/manifest.ts +34 -0
  30. package/src/core/meta-tools.ts +128 -18
  31. package/src/core/middlewares/capability-gate.ts +130 -12
  32. package/src/core/middlewares/context-guard.ts +55 -24
  33. package/src/core/middlewares/index.ts +20 -2
  34. package/src/core/middlewares/page-context.ts +46 -2
  35. package/src/core/middlewares/return-direct-first.ts +47 -0
  36. package/src/core/middlewares/summarization.ts +152 -10
  37. package/src/core/middlewares/tool-execution.ts +323 -0
  38. package/src/core/middlewares/tool-repetition-guard.ts +202 -35
  39. package/src/core/middlewares/turn-boundary.ts +162 -0
  40. package/src/core/openrouter-pricing.ts +155 -22
  41. package/src/core/prompt-composer.ts +112 -7
  42. package/src/core/registries.ts +253 -48
  43. package/src/core/request-admission.ts +159 -0
  44. package/src/core/runtime-context.ts +107 -8
  45. package/src/core/state.ts +1 -1
  46. package/src/core/sub-agent-fallback.ts +21 -2
  47. package/src/core/subagent-as-tool.ts +78 -43
  48. package/src/core/tool-access.ts +189 -0
  49. package/src/core/tool-scheduler.ts +125 -0
  50. package/src/core/turn-budget.ts +205 -0
  51. package/src/core/turn-time-note.ts +43 -0
  52. package/src/core/user-kv.ts +446 -0
  53. package/src/core/wrap-plugin-tool.ts +20 -0
  54. package/src/delivery/plan.ts +278 -0
  55. package/src/delivery/profile.ts +161 -0
  56. package/src/delivery/prompt.ts +27 -0
  57. package/src/delivery/schema.ts +61 -0
  58. package/src/delivery/shaper.ts +389 -0
  59. package/src/delivery/types.ts +84 -0
  60. package/src/do/ambient.ts +51 -15
  61. package/src/do/contracts.ts +198 -6
  62. package/src/do/idle-eviction.ts +38 -0
  63. package/src/do/matrix-turn-ledger.ts +24 -5
  64. package/src/do/result-store-test-do.ts +137 -3
  65. package/src/do/result-store.ts +239 -58
  66. package/src/do/room-mirror.ts +29 -0
  67. package/src/do/run-buffer.ts +64 -9
  68. package/src/do/run-coordinator.ts +451 -76
  69. package/src/do/run-request.ts +2 -0
  70. package/src/do/run-store-test-do.ts +301 -1
  71. package/src/do/run-store.ts +385 -43
  72. package/src/do/secrets-adapter.ts +22 -3
  73. package/src/do/sse-stream.ts +60 -9
  74. package/src/do/transcript.ts +48 -5
  75. package/src/do/turn-body.ts +105 -5
  76. package/src/do/ucan-service.ts +204 -34
  77. package/src/do/user-oracle-do.ts +1883 -264
  78. package/src/feedback/config.ts +127 -0
  79. package/src/feedback/contract.ts +148 -0
  80. package/src/feedback/linear-sink.ts +285 -0
  81. package/src/feedback/privacy.ts +150 -0
  82. package/src/feedback/reservation.ts +298 -0
  83. package/src/feedback/submit.ts +247 -0
  84. package/src/index.ts +138 -6
  85. package/src/llm/byo-adapter.ts +27 -6
  86. package/src/llm/byo-catalog.ts +13 -0
  87. package/src/llm/byo-model-fallback.ts +248 -0
  88. package/src/llm/byo-service.ts +45 -8
  89. package/src/llm/provider-error.ts +139 -2
  90. package/src/matrix/gateway-do.ts +523 -111
  91. package/src/matrix/group-chat.ts +66 -23
  92. package/src/matrix/inbox-store.ts +16 -1
  93. package/src/matrix/ingest.ts +79 -14
  94. package/src/matrix/media-deadline.ts +130 -0
  95. package/src/matrix/replay-format.ts +5 -3
  96. package/src/matrix/reply-parts.ts +92 -0
  97. package/src/matrix/room-alias.ts +96 -0
  98. package/src/matrix/user-homeserver.ts +166 -7
  99. package/src/owner-store/ixo-vfs-store.ts +170 -43
  100. package/src/owner-store/migrating-store.ts +31 -15
  101. package/src/owner-store/types.ts +18 -3
  102. package/src/plugin-api/index.ts +43 -2
  103. package/src/plugin-api/oracle-plugin.ts +35 -0
  104. package/src/plugin-api/request-admission.ts +41 -0
  105. package/src/plugin-api/tool-helper.ts +25 -1
  106. package/src/plugin-api/tool-plane.ts +92 -0
  107. package/src/plugin-api/types.ts +194 -6
  108. package/src/plugins/agui/agui.plugin.ts +31 -16
  109. package/src/plugins/composio/composio-tools.ts +57 -6
  110. package/src/plugins/composio/composio-ucan.ts +3 -2
  111. package/src/plugins/composio/composio.plugin.ts +2 -0
  112. package/src/plugins/delegated-capabilities.ts +25 -0
  113. package/src/plugins/editor/content-session.ts +173 -83
  114. package/src/plugins/editor/content-tools.ts +76 -17
  115. package/src/plugins/editor/editor-mx.ts +28 -9
  116. package/src/plugins/editor/editor.plugin.ts +3 -1
  117. package/src/plugins/editor/failures.ts +28 -7
  118. package/src/plugins/editor/prompts.ts +2 -1
  119. package/src/plugins/editor/provider.ts +553 -40
  120. package/src/plugins/editor/standalone-editor-tool.ts +61 -18
  121. package/src/plugins/firecrawl/firecrawl-tools.ts +13 -5
  122. package/src/plugins/flows/action-metadata.ts +111 -11
  123. package/src/plugins/flows/actions.ts +18 -0
  124. package/src/plugins/flows/decision-context.ts +31 -0
  125. package/src/plugins/flows/edit.ts +223 -52
  126. package/src/plugins/flows/errors.ts +22 -1
  127. package/src/plugins/flows/flow-doc.ts +92 -9
  128. package/src/plugins/flows/flows.plugin.ts +11 -5
  129. package/src/plugins/flows/forms.ts +34 -0
  130. package/src/plugins/flows/index.ts +2 -0
  131. package/src/plugins/flows/input-policy.ts +140 -0
  132. package/src/plugins/flows/prompts.ts +14 -11
  133. package/src/plugins/flows/read.ts +49 -9
  134. package/src/plugins/flows/ref-syntax.ts +129 -0
  135. package/src/plugins/flows/test-homeserver.ts +245 -0
  136. package/src/plugins/flows/tools/authoring.ts +216 -112
  137. package/src/plugins/flows/tools/discovery.ts +4 -1
  138. package/src/plugins/flows/tools/forms.ts +21 -2
  139. package/src/plugins/flows/tools/settings.ts +34 -13
  140. package/src/plugins/flows/translator.ts +13 -10
  141. package/src/plugins/flows/types.ts +167 -42
  142. package/src/plugins/index.ts +11 -1
  143. package/src/plugins/ixo-transaction/chain-ids.ts +43 -0
  144. package/src/plugins/ixo-transaction/index.ts +2 -0
  145. package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +119 -0
  146. package/src/plugins/ixo-transaction/receipts.ts +99 -0
  147. package/src/plugins/ixo-transaction/tools.ts +329 -0
  148. package/src/plugins/ixo-transaction/wallet-signing.ts +202 -0
  149. package/src/plugins/mcp-tool-adapter.ts +23 -2
  150. package/src/plugins/memory/memory-tools.ts +7 -2
  151. package/src/plugins/memory/memory-ucan.ts +3 -2
  152. package/src/plugins/memory/memory.plugin.ts +2 -0
  153. package/src/plugins/pod-creator/blueprint-store.ts +163 -0
  154. package/src/plugins/pod-creator/blueprint-types.ts +61 -0
  155. package/src/plugins/pod-creator/bounded-map.ts +87 -0
  156. package/src/plugins/pod-creator/capsule-content-client.ts +285 -0
  157. package/src/plugins/pod-creator/chain-gateway.ts +110 -0
  158. package/src/plugins/pod-creator/config.ts +85 -0
  159. package/src/plugins/pod-creator/create-session-store.ts +180 -0
  160. package/src/plugins/pod-creator/create-tools.ts +538 -0
  161. package/src/plugins/pod-creator/design-pod-roles.ts +111 -0
  162. package/src/plugins/pod-creator/index.ts +65 -0
  163. package/src/plugins/pod-creator/orchestration-tools.ts +172 -0
  164. package/src/plugins/pod-creator/pod-creator.plugin.ts +190 -0
  165. package/src/plugins/pod-creator/stage.ts +201 -0
  166. package/src/plugins/pod-creator/sub-agents.ts +189 -0
  167. package/src/plugins/portal/action-log.ts +21 -1
  168. package/src/plugins/portal/declared-tools.ts +90 -0
  169. package/src/plugins/portal/portal.plugin.ts +42 -16
  170. package/src/plugins/sandbox/sandbox-bridge.ts +79 -10
  171. package/src/plugins/sandbox/sandbox-mcp.ts +3 -2
  172. package/src/plugins/sandbox/sandbox-write-blob.ts +11 -1
  173. package/src/plugins/sandbox/sandbox.plugin.ts +8 -16
  174. package/src/plugins/tasks/manifest.ts +1 -1
  175. package/src/plugins/tasks/middleware.ts +20 -7
  176. package/src/plugins/tasks/tasks-tools.ts +30 -3
  177. package/src/plugins/tasks/tasks.plugin.ts +64 -11
  178. package/src/plugins/user-preferences/schema.ts +12 -1
  179. package/src/plugins/user-preferences/user-preferences-store.ts +38 -13
  180. package/src/plugins/user-preferences/user-preferences-tool.ts +3 -2
  181. package/src/plugins/vfs/vfs-client.ts +250 -71
  182. package/src/plugins/vfs/vfs-content.ts +30 -11
  183. package/src/plugins/vfs/vfs-errors.ts +22 -0
  184. package/src/plugins/vfs/vfs-sandbox-tools.ts +12 -5
  185. package/src/plugins/vfs/vfs-tools.ts +60 -20
  186. package/src/realtime/frontend-call-registry.ts +228 -81
  187. package/src/realtime/realtime-endpoint.ts +215 -44
  188. package/src/realtime/session-socket-hub.ts +47 -2
  189. package/src/secrets/secrets-service.ts +9 -3
  190. package/src/shell/app.ts +529 -82
  191. package/src/shell/auth.ts +131 -37
  192. package/src/shell/turn-body-cap.ts +11 -13
  193. package/src/sqlite/blob-compactor.ts +61 -28
  194. package/src/sqlite/database.ts +156 -90
  195. package/src/sqlite/do-vfs.ts +416 -129
  196. package/src/sqlite/page-tier.ts +133 -40
  197. package/src/sqlite/serialization.ts +40 -0
  198. package/src/sqlite/sessions-store.ts +10 -2
  199. package/src/sqlite/sqlite-saver.ts +139 -19
  200. package/src/sqlite/user-kv-store.ts +317 -0
  201. package/src/tasks/schedule.ts +201 -12
  202. package/src/tasks/scheduler.ts +586 -98
  203. package/src/tasks/spec.ts +67 -3
  204. package/src/tasks/store.ts +292 -27
  205. 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
- * - Fallback: `x-ucan-delegation: <base64 CAR>` — accepted alone for legacy
9
- * clients and always used for downstream authorization when present. A
10
- * delegation is only trusted when its issuer equals the authenticated DID.
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
- InMemoryInvocationStore,
30
+ type DIDKeyResolver,
31
+ type InvocationStore,
22
32
  } from '@ixo/ucan';
23
33
 
24
34
  /**
25
- * One replay-protection store per isolate, WITHOUT the library's hourly
26
- * `setInterval` sweep: a pending timer keeps a Durable Object resident, and
27
- * this code also runs inside the user object (socket CONNECT auth), where a
28
- * leaked interval per CONNECT blocked WebSocket hibernation entirely. Expired
29
- * entries are dropped lazily on lookup and swept every N validations instead.
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 invocationStore = new InMemoryInvocationStore({
32
- enableAutoCleanup: false,
33
- });
34
- const SWEEP_EVERY = 500;
35
- let validations = 0;
43
+ const reusableInvocations: InvocationStore = {
44
+ has: async () => false,
45
+ add: async () => undefined,
46
+ addIfAbsent: async () => true,
47
+ };
36
48
 
37
- function sweepInvocationStore(): void {
38
- validations += 1;
39
- if (validations % SWEEP_EVERY === 0) void invocationStore.cleanup();
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
- sweepInvocationStore();
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: createIxoDIDResolver({ indexerUrl: cfg.blocksyncUri }),
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
- async function validateDelegation(
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?: number }
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: createIxoDIDResolver({ indexerUrl: cfg.blocksyncUri }),
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 reads the body in
5
- * full before forwarding it to the user object. It no longer mirrors the Node
6
- * runtime, which keeps Express body-parser's 100 kb default — a normal Portal
7
- * turn now exceeds that. Every turn carries the browser-tool catalogue (26–32
8
- * tools, 65–70 KiB of JSON schema) and the AG-UI action schemas (~27 KiB)
9
- * before the message text, and a compose-topic turn adds the pinned text in
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 Matrix
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 rewrite the compressible ones as
57
- * gzipped blobs. Call repeatedly (persisting `cursors`) until `done`.
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
- const rows = await db.exec<{ rowid: number; data: Uint8Array | string }>(
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, budget],
113
+ LIMIT 1`,
114
+ [cursor, MIN_COMPRESS_BYTES],
93
115
  );
94
- if (rows.length === 0) break;
95
- for (const { rowid, data } of rows) {
96
- cursor = Math.max(cursor, rowid);
97
- budget -= 1;
98
- examined += 1;
99
- const bytes =
100
- typeof data === 'string' ? textEncoder.encode(data) : data;
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
- // Budget ran out mid-table — cannot know whether scans are exhausted.
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 { rewritten, savedBytes, examined, done: exhausted, cursors: next };
148
+ return {
149
+ rewritten,
150
+ savedBytes,
151
+ examined,
152
+ examinedBytes,
153
+ done: exhausted,
154
+ cursors: next,
155
+ };
123
156
  }
124
157
 
125
158
  /**