@ixo/oracle-runtime-workers 0.13.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 +13 -8
  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 +170 -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 +504 -108
  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
@@ -0,0 +1,446 @@
1
+ /**
2
+ * Shared pieces of the `ctx.kv` surface (`UserKvSurface`): argument checks,
3
+ * JSON encoding, the hard storage caps, and an in-memory implementation for
4
+ * tests and partial hosts. The Workers host backs the surface with the
5
+ * user's SQLite file instead (`sqlite/user-kv-store.ts`), with the same
6
+ * semantics.
7
+ *
8
+ * The caps exist because the rows share a file with the user's LangGraph
9
+ * checkpoints, sessions and transcript, and that file is exported as the
10
+ * owner copy and re-imported on a cold start: a plugin must not be able to
11
+ * grow it without bound, whatever options it passes.
12
+ */
13
+ import type { UserKvSurface, UserKvWriteOptions } from '../plugin-api/types';
14
+
15
+ /** Longest namespace / key accepted, in characters. */
16
+ export const USER_KV_MAX_NAME_LENGTH = 512;
17
+
18
+ /** Largest stored value: UTF-8 bytes of its JSON text. */
19
+ export const USER_KV_MAX_VALUE_BYTES = 256 * 1024;
20
+ /** Most rows one namespace may hold (also the largest `maxEntries`). */
21
+ export const USER_KV_MAX_ENTRIES_PER_NAMESPACE = 10_000;
22
+ /** Most rows the user's store may hold across all namespaces. */
23
+ export const USER_KV_MAX_TOTAL_ENTRIES = 50_000;
24
+ /**
25
+ * Most bytes the user's store may hold across all namespaces, counting each
26
+ * row's namespace, key and value JSON ({@link userKvRowBytes}).
27
+ */
28
+ export const USER_KV_MAX_TOTAL_BYTES = 32 * 1024 * 1024;
29
+
30
+ /** The hard caps one store enforces. */
31
+ export interface UserKvLimits {
32
+ maxValueBytes: number;
33
+ maxEntriesPerNamespace: number;
34
+ maxTotalEntries: number;
35
+ maxTotalBytes: number;
36
+ }
37
+
38
+ export const USER_KV_LIMITS: Readonly<UserKvLimits> = Object.freeze({
39
+ maxValueBytes: USER_KV_MAX_VALUE_BYTES,
40
+ maxEntriesPerNamespace: USER_KV_MAX_ENTRIES_PER_NAMESPACE,
41
+ maxTotalEntries: USER_KV_MAX_TOTAL_ENTRIES,
42
+ maxTotalBytes: USER_KV_MAX_TOTAL_BYTES,
43
+ });
44
+
45
+ /**
46
+ * The caps a store runs with: the defaults, with `overrides` (tests only;
47
+ * the host never passes any) replacing individual ones.
48
+ */
49
+ export function resolveUserKvLimits(
50
+ overrides: Partial<UserKvLimits> = {},
51
+ ): Readonly<UserKvLimits> {
52
+ const limits = { ...USER_KV_LIMITS, ...overrides };
53
+ for (const [name, value] of Object.entries(limits)) {
54
+ if (!Number.isInteger(value) || value < 1) {
55
+ throw new RangeError(
56
+ `user kv limit ${name} must be a positive integer, got ${value}`,
57
+ );
58
+ }
59
+ }
60
+ return Object.freeze(limits);
61
+ }
62
+
63
+ /** Which cap a refused write would have broken. */
64
+ export type UserKvLimitKind =
65
+ | 'value'
66
+ | 'namespace-entries'
67
+ | 'entries'
68
+ | 'bytes';
69
+
70
+ /**
71
+ * A write `ctx.kv` refused because it would break a hard cap. Nothing was
72
+ * written and nothing was evicted: the previous value, if any, is still in
73
+ * place. `actual` is what the store (or, for `value`, the value) would have
74
+ * measured after the write; `max` is the cap.
75
+ */
76
+ export class UserKvLimitError extends Error {
77
+ override readonly name = 'UserKvLimitError';
78
+
79
+ constructor(
80
+ readonly limit: UserKvLimitKind,
81
+ readonly namespace: string,
82
+ readonly max: number,
83
+ readonly actual: number,
84
+ ) {
85
+ super(userKvLimitMessage(limit, namespace, max, actual));
86
+ }
87
+ }
88
+
89
+ function userKvLimitMessage(
90
+ limit: UserKvLimitKind,
91
+ namespace: string,
92
+ max: number,
93
+ actual: number,
94
+ ): string {
95
+ const where = `user kv write to namespace "${namespace}" refused`;
96
+ switch (limit) {
97
+ case 'value':
98
+ return `${where}: the value is ${actual} bytes of JSON, the cap is ${max}`;
99
+ case 'namespace-entries':
100
+ return `${where}: the namespace would hold ${actual} entries, the cap is ${max}`;
101
+ case 'entries':
102
+ return `${where}: the store would hold ${actual} entries across all namespaces, the cap is ${max}`;
103
+ case 'bytes':
104
+ return `${where}: the store would hold ${actual} bytes of namespace, key and value across all namespaces, the cap is ${max}`;
105
+ }
106
+ }
107
+
108
+ const utf8 = new TextEncoder();
109
+
110
+ /** UTF-8 byte length of a stored value's JSON text — what the value cap counts. */
111
+ export function userKvValueBytes(json: string): number {
112
+ return utf8.encode(json).byteLength;
113
+ }
114
+
115
+ /**
116
+ * What one row adds to the store's byte total: UTF-8 bytes of its namespace,
117
+ * key and value JSON. Names count because each may be up to
118
+ * {@link USER_KV_MAX_NAME_LENGTH} characters, enough to carry far more than
119
+ * the byte cap across the entry cap if only values were counted.
120
+ */
121
+ export function userKvRowBytes(
122
+ namespace: string,
123
+ key: string,
124
+ valueBytes: number,
125
+ ): number {
126
+ return (
127
+ utf8.encode(namespace).byteLength + utf8.encode(key).byteLength + valueBytes
128
+ );
129
+ }
130
+
131
+ /** The value's byte length; throws {@link UserKvLimitError} above the cap. */
132
+ export function assertUserKvValueSize(
133
+ namespace: string,
134
+ json: string,
135
+ limits: Readonly<UserKvLimits>,
136
+ ): number {
137
+ const bytes = userKvValueBytes(json);
138
+ if (bytes > limits.maxValueBytes) {
139
+ throw new UserKvLimitError('value', namespace, limits.maxValueBytes, bytes);
140
+ }
141
+ return bytes;
142
+ }
143
+
144
+ /** What a store holds after a write, as far as the caps are concerned. */
145
+ export interface UserKvUsage {
146
+ namespaceEntries: number;
147
+ totalEntries: number;
148
+ totalBytes: number;
149
+ }
150
+
151
+ /**
152
+ * The first cap a write breaks, or undefined. Only a measure the write grew
153
+ * is checked — a write that added a key for the entry counts, one that grew
154
+ * the stored bytes for the byte total — so a store already above a cap
155
+ * (a file written before the caps existed) can still shrink, replace in
156
+ * place and delete.
157
+ */
158
+ export function userKvOverflow(
159
+ namespace: string,
160
+ usage: UserKvUsage,
161
+ grew: { entries: boolean; bytes: boolean },
162
+ limits: Readonly<UserKvLimits>,
163
+ ): UserKvLimitError | undefined {
164
+ if (grew.entries) {
165
+ if (usage.namespaceEntries > limits.maxEntriesPerNamespace) {
166
+ return new UserKvLimitError(
167
+ 'namespace-entries',
168
+ namespace,
169
+ limits.maxEntriesPerNamespace,
170
+ usage.namespaceEntries,
171
+ );
172
+ }
173
+ if (usage.totalEntries > limits.maxTotalEntries) {
174
+ return new UserKvLimitError(
175
+ 'entries',
176
+ namespace,
177
+ limits.maxTotalEntries,
178
+ usage.totalEntries,
179
+ );
180
+ }
181
+ }
182
+ if (grew.bytes && usage.totalBytes > limits.maxTotalBytes) {
183
+ return new UserKvLimitError(
184
+ 'bytes',
185
+ namespace,
186
+ limits.maxTotalBytes,
187
+ usage.totalBytes,
188
+ );
189
+ }
190
+ return undefined;
191
+ }
192
+
193
+ export function assertUserKvName(
194
+ kind: 'namespace' | 'key',
195
+ value: string,
196
+ ): void {
197
+ if (value.length === 0 || value.length > USER_KV_MAX_NAME_LENGTH) {
198
+ throw new RangeError(
199
+ `user kv ${kind} must be 1-${USER_KV_MAX_NAME_LENGTH} characters, got ${value.length}`,
200
+ );
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Validate a write's options. A `maxEntries` above the per-namespace cap is
206
+ * refused rather than clamped, so a plugin learns its bound is not the one
207
+ * in force.
208
+ */
209
+ export function assertUserKvOptions(
210
+ options: UserKvWriteOptions,
211
+ limits: Readonly<UserKvLimits> = USER_KV_LIMITS,
212
+ ): void {
213
+ const { idleTtlMs, maxEntries } = options;
214
+ if (
215
+ idleTtlMs !== undefined &&
216
+ (!Number.isFinite(idleTtlMs) || idleTtlMs <= 0)
217
+ ) {
218
+ throw new RangeError(
219
+ `user kv idleTtlMs must be a positive number, got ${idleTtlMs}`,
220
+ );
221
+ }
222
+ if (
223
+ maxEntries !== undefined &&
224
+ (!Number.isInteger(maxEntries) || maxEntries < 1)
225
+ ) {
226
+ throw new RangeError(
227
+ `user kv maxEntries must be a positive integer, got ${maxEntries}`,
228
+ );
229
+ }
230
+ if (maxEntries !== undefined && maxEntries > limits.maxEntriesPerNamespace) {
231
+ throw new RangeError(
232
+ `user kv maxEntries must be at most ${limits.maxEntriesPerNamespace} (the per-namespace cap), got ${maxEntries}`,
233
+ );
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Serialise a value for storage. Rejects what JSON cannot carry (a bare
239
+ * `undefined`, a function, a symbol) instead of storing something the next
240
+ * read cannot return.
241
+ */
242
+ export function encodeUserKvValue(value: unknown): string {
243
+ const json = JSON.stringify(value);
244
+ if (typeof json !== 'string') {
245
+ throw new TypeError('user kv values must be JSON-serialisable');
246
+ }
247
+ return json;
248
+ }
249
+
250
+ export function decodeUserKvValue(json: string): unknown {
251
+ return JSON.parse(json);
252
+ }
253
+
254
+ interface MemoryEntry {
255
+ json: string;
256
+ /** Row bytes ({@link userKvRowBytes}): namespace + key + value JSON. */
257
+ bytes: number;
258
+ idleTtlMs?: number;
259
+ expiresAt?: number;
260
+ }
261
+
262
+ /**
263
+ * In-memory {@link UserKvSurface}: per-namespace maps whose insertion order
264
+ * is recency (a hit re-inserts the entry). Expiry is lazy, enforced on access
265
+ * and on bounded writes. Values are stored serialised, so callers get copies.
266
+ * Enforces the same hard caps as the SQLite store; `limits` overrides them
267
+ * for tests.
268
+ */
269
+ export function createMemoryUserKv(
270
+ options: { now?: () => number; limits?: Partial<UserKvLimits> } = {},
271
+ ): UserKvSurface {
272
+ const now = options.now ?? Date.now;
273
+ const limits = resolveUserKvLimits(options.limits);
274
+ const namespaces = new Map<string, Map<string, MemoryEntry>>();
275
+
276
+ const bucket = (namespace: string): Map<string, MemoryEntry> => {
277
+ let entries = namespaces.get(namespace);
278
+ if (!entries) {
279
+ entries = new Map();
280
+ namespaces.set(namespace, entries);
281
+ }
282
+ return entries;
283
+ };
284
+
285
+ const expired = (entry: MemoryEntry, cutoff: number): boolean =>
286
+ entry.expiresAt !== undefined && entry.expiresAt <= cutoff;
287
+
288
+ /** The live entry (refreshed and marked most recently used), or undefined. */
289
+ const touch = (
290
+ entries: Map<string, MemoryEntry>,
291
+ key: string,
292
+ ): MemoryEntry | undefined => {
293
+ const entry = entries.get(key);
294
+ if (!entry) return undefined;
295
+ entries.delete(key);
296
+ if (expired(entry, now())) return undefined;
297
+ if (entry.idleTtlMs !== undefined) {
298
+ entry.expiresAt = now() + entry.idleTtlMs;
299
+ }
300
+ entries.set(key, entry);
301
+ return entry;
302
+ };
303
+
304
+ const usage = (entries: Map<string, MemoryEntry>): UserKvUsage => {
305
+ let totalEntries = 0;
306
+ let totalBytes = 0;
307
+ for (const ns of namespaces.values()) {
308
+ totalEntries += ns.size;
309
+ for (const entry of ns.values()) totalBytes += entry.bytes;
310
+ }
311
+ return { namespaceEntries: entries.size, totalEntries, totalBytes };
312
+ };
313
+
314
+ /** Drop expired entries of every namespace; returns how many went. */
315
+ const sweepExpired = (): number => {
316
+ const cutoff = now();
317
+ let swept = 0;
318
+ for (const ns of namespaces.values()) {
319
+ for (const [key, entry] of ns) {
320
+ if (expired(entry, cutoff)) {
321
+ ns.delete(key);
322
+ swept++;
323
+ }
324
+ }
325
+ }
326
+ return swept;
327
+ };
328
+
329
+ /**
330
+ * Run `body` against `entries` all-or-nothing: if it throws, the namespace
331
+ * is put back exactly as it was (order, values, deadlines), the way the
332
+ * SQLite store's transaction rolls back.
333
+ */
334
+ const atomically = <T>(
335
+ entries: Map<string, MemoryEntry>,
336
+ body: () => T,
337
+ ): T => {
338
+ const before = [...entries].map(([key, entry]): [string, MemoryEntry] => [
339
+ key,
340
+ { ...entry },
341
+ ]);
342
+ try {
343
+ return body();
344
+ } catch (error) {
345
+ entries.clear();
346
+ for (const [key, entry] of before) entries.set(key, entry);
347
+ throw error;
348
+ }
349
+ };
350
+
351
+ /**
352
+ * Replace (or, for `json === undefined`, delete) the entry, apply the
353
+ * write's own `maxEntries` trim, then refuse with {@link UserKvLimitError}
354
+ * if a hard cap is broken even after the store-wide expired entries are
355
+ * swept. Callers run it inside {@link atomically}.
356
+ */
357
+ const write = (
358
+ namespace: string,
359
+ entries: Map<string, MemoryEntry>,
360
+ key: string,
361
+ json: string | undefined,
362
+ opts: UserKvWriteOptions,
363
+ ): void => {
364
+ const previous = entries.get(key);
365
+ const live =
366
+ previous !== undefined && !expired(previous, now())
367
+ ? previous
368
+ : undefined;
369
+ entries.delete(key);
370
+ let bytes = 0;
371
+ if (json !== undefined) {
372
+ bytes = userKvRowBytes(
373
+ namespace,
374
+ key,
375
+ assertUserKvValueSize(namespace, json, limits),
376
+ );
377
+ const entry: MemoryEntry = { json, bytes };
378
+ if (opts.idleTtlMs !== undefined) {
379
+ entry.idleTtlMs = opts.idleTtlMs;
380
+ entry.expiresAt = now() + opts.idleTtlMs;
381
+ }
382
+ entries.set(key, entry);
383
+ }
384
+ if (opts.maxEntries !== undefined) {
385
+ const cutoff = now();
386
+ for (const [k, entry] of entries) {
387
+ if (expired(entry, cutoff)) entries.delete(k);
388
+ }
389
+ while (entries.size > opts.maxEntries) {
390
+ const oldest = entries.keys().next();
391
+ if (oldest.done) break;
392
+ entries.delete(oldest.value);
393
+ }
394
+ }
395
+ if (json === undefined) return;
396
+ // The replaced row has the same namespace and key, so comparing row
397
+ // bytes compares value bytes.
398
+ const grew = {
399
+ entries: live === undefined,
400
+ bytes: bytes > (live?.bytes ?? 0),
401
+ };
402
+ if (!grew.entries && !grew.bytes) return;
403
+ let overflow = userKvOverflow(namespace, usage(entries), grew, limits);
404
+ if (overflow && sweepExpired() > 0) {
405
+ overflow = userKvOverflow(namespace, usage(entries), grew, limits);
406
+ }
407
+ if (overflow) throw overflow;
408
+ };
409
+
410
+ return {
411
+ async get(namespace, key) {
412
+ assertUserKvName('namespace', namespace);
413
+ assertUserKvName('key', key);
414
+ const entry = touch(bucket(namespace), key);
415
+ return entry ? decodeUserKvValue(entry.json) : undefined;
416
+ },
417
+ async set(namespace, key, value, opts = {}) {
418
+ assertUserKvName('namespace', namespace);
419
+ assertUserKvName('key', key);
420
+ assertUserKvOptions(opts, limits);
421
+ const json = encodeUserKvValue(value);
422
+ const entries = bucket(namespace);
423
+ atomically(entries, () => write(namespace, entries, key, json, opts));
424
+ },
425
+ async update(namespace, key, fn, opts = {}) {
426
+ assertUserKvName('namespace', namespace);
427
+ assertUserKvName('key', key);
428
+ assertUserKvOptions(opts, limits);
429
+ const entries = bucket(namespace);
430
+ const json = atomically(entries, () => {
431
+ const entry = touch(entries, key);
432
+ const next = fn(entry ? decodeUserKvValue(entry.json) : undefined);
433
+ const encoded =
434
+ next === undefined ? undefined : encodeUserKvValue(next);
435
+ write(namespace, entries, key, encoded, opts);
436
+ return encoded;
437
+ });
438
+ return json === undefined ? undefined : decodeUserKvValue(json);
439
+ },
440
+ async delete(namespace, key) {
441
+ assertUserKvName('namespace', namespace);
442
+ assertUserKvName('key', key);
443
+ bucket(namespace).delete(key);
444
+ },
445
+ };
446
+ }
@@ -2,6 +2,7 @@ import { Command } from '@langchain/langgraph';
2
2
  import { capToolResult, type ResultCapConfig } from './middlewares/result-cap';
3
3
  import { tool } from '@langchain/core/tools';
4
4
  import type { StructuredTool } from 'langchain';
5
+ import { requireToolPlane, toolPlaneOf } from '../plugin-api/tool-plane';
5
6
  import type {
6
7
  PluginTool,
7
8
  RuntimeContext,
@@ -23,6 +24,12 @@ export interface WrapPluginToolOptions {
23
24
  ambient: AmbientServices;
24
25
  /** Snapshot of the graph state for the in-flight build. */
25
26
  state: RuntimeStateInput;
27
+ /**
28
+ * The contributing plugin. It names an admin tool's capability
29
+ * (`adminToolResource`), so an admin tool cannot be wrapped without it;
30
+ * runtime tools (the meta-tools) omit it.
31
+ */
32
+ pluginName?: string;
26
33
  /** Plugin manifest title used to auto-prefix the description. */
27
34
  pluginTitle?: string;
28
35
  /**
@@ -76,6 +83,11 @@ export function resolveRunConfig(
76
83
  *
77
84
  * The agent-facing description is auto-prefixed with the plugin's
78
85
  * `manifest.title` so the agent always knows which plugin a tool belongs to.
86
+ *
87
+ * An admin-plane tool is checked against the call's delegation right before
88
+ * its handler runs (`requireToolPlane`), whatever discovery and binding
89
+ * decided: a host that binds it without the turn's filtering still cannot
90
+ * run it for a user whose delegation does not grant it.
79
91
  */
80
92
  export function wrapPluginTool(
81
93
  pluginTool: PluginTool,
@@ -84,11 +96,17 @@ export function wrapPluginTool(
84
96
  const {
85
97
  ambient,
86
98
  state,
99
+ pluginName,
87
100
  pluginTitle,
88
101
  sharedFactory,
89
102
  fallbackContext,
90
103
  resultCap,
91
104
  } = options;
105
+ if (toolPlaneOf(pluginTool) === 'admin' && pluginName === undefined) {
106
+ throw new Error(
107
+ `wrapPluginTool: admin tool "${pluginTool.name}" needs \`pluginName\` — its capability is named by plugin and tool.`,
108
+ );
109
+ }
92
110
  const description = pluginTitle
93
111
  ? `[${pluginTitle}] ${pluginTool.description}`
94
112
  : pluginTool.description;
@@ -104,6 +122,8 @@ export function wrapPluginTool(
104
122
  state,
105
123
  sharedFactory,
106
124
  );
125
+ if (pluginName !== undefined)
126
+ requireToolPlane(ctx, pluginName, pluginTool);
107
127
  const output = await pluginTool.handler(args, ctx);
108
128
  if (!resultCap || output instanceof Command || output == null)
109
129
  return output;