@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.
- package/README.md +32 -10
- package/package.json +13 -8
- 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 +170 -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 +504 -108
- 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
|
@@ -20,7 +20,11 @@ import type {
|
|
|
20
20
|
PluginTool,
|
|
21
21
|
RuntimeContext,
|
|
22
22
|
} from '../../plugin-api/types';
|
|
23
|
-
import {
|
|
23
|
+
import { logFrontendAction } from '../portal/action-log';
|
|
24
|
+
import {
|
|
25
|
+
describeDropped,
|
|
26
|
+
sanitizeDeclaredTools,
|
|
27
|
+
} from '../portal/declared-tools';
|
|
24
28
|
import { createAguiSubAgent } from './agui-agent';
|
|
25
29
|
|
|
26
30
|
const manifest: PluginManifest = {
|
|
@@ -60,12 +64,16 @@ const AG_ACTION_SHAPE = z.object({
|
|
|
60
64
|
|
|
61
65
|
const ARGS_RECORD_SHAPE = z.record(z.string(), z.unknown());
|
|
62
66
|
|
|
67
|
+
/** The client's AG-UI actions; a name declared twice keeps its first descriptor. */
|
|
63
68
|
export function parseAgActions(value: unknown): AgAction[] {
|
|
64
69
|
if (!Array.isArray(value)) return [];
|
|
65
70
|
const out: AgAction[] = [];
|
|
71
|
+
const names = new Set<string>();
|
|
66
72
|
for (const entry of value) {
|
|
67
73
|
const parsed = AG_ACTION_SHAPE.safeParse(entry);
|
|
68
|
-
if (parsed.success
|
|
74
|
+
if (!parsed.success || names.has(parsed.data.name)) continue;
|
|
75
|
+
names.add(parsed.data.name);
|
|
76
|
+
out.push(parsed.data);
|
|
69
77
|
}
|
|
70
78
|
return out;
|
|
71
79
|
}
|
|
@@ -95,36 +103,43 @@ export function buildActionTool(action: AgAction): PluginTool | null {
|
|
|
95
103
|
`No browser is connected to session ${sessionId}, so AG-UI action ${action.name} cannot run — the client must open the realtime (socket.io) channel for this session first.`,
|
|
96
104
|
);
|
|
97
105
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const toolCallId = `ag_${requestId || 'noreq'}_${crypto
|
|
101
|
-
.randomUUID()
|
|
102
|
-
.slice(0, 8)}`;
|
|
106
|
+
// The bridge makes every invocation unique (`ag_<requestId>:<uuid>`).
|
|
107
|
+
let invocationId: string | undefined;
|
|
103
108
|
const result = await frontend.callAgAction({
|
|
104
109
|
sessionId,
|
|
105
|
-
toolCallId
|
|
110
|
+
toolCallId: `ag_${ctx.session.requestId || 'noreq'}`,
|
|
106
111
|
toolName: action.name,
|
|
107
|
-
args,
|
|
112
|
+
args: parseArgs(input),
|
|
108
113
|
timeoutMs: AG_ACTION_TIMEOUT_MS,
|
|
114
|
+
signal: ctx.abortSignal,
|
|
115
|
+
onInvocation: (id) => {
|
|
116
|
+
invocationId = id;
|
|
117
|
+
},
|
|
109
118
|
});
|
|
110
|
-
|
|
111
|
-
name: action.name,
|
|
112
|
-
args,
|
|
113
|
-
result,
|
|
114
|
-
success: true,
|
|
115
|
-
});
|
|
119
|
+
logFrontendAction(ctx, { name: action.name, invocationId, result });
|
|
116
120
|
return JSON.stringify(result);
|
|
117
121
|
},
|
|
118
122
|
{
|
|
119
123
|
name: action.name,
|
|
120
124
|
description: action.description,
|
|
121
125
|
schema,
|
|
126
|
+
// A UI action on the user's screen: running it again is a new action.
|
|
127
|
+
repeatable: true,
|
|
122
128
|
},
|
|
123
129
|
);
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
/**
|
|
133
|
+
* The AG-UI actions declared on this request that may become tools: invalid,
|
|
134
|
+
* duplicate and oversized declarations are dropped (`declared-tools.ts`).
|
|
135
|
+
*/
|
|
126
136
|
function readAgActions(rtCtx: RuntimeContext): AgAction[] {
|
|
127
|
-
|
|
137
|
+
const { kept, dropped } = sanitizeDeclaredTools(
|
|
138
|
+
parseAgActions(rtCtx.history.state.agActions),
|
|
139
|
+
);
|
|
140
|
+
const summary = describeDropped('AG-UI actions', dropped);
|
|
141
|
+
if (summary) rtCtx.logger.warn(summary);
|
|
142
|
+
return kept;
|
|
128
143
|
}
|
|
129
144
|
|
|
130
145
|
export class AGUIPlugin extends OraclePlugin {
|
|
@@ -2,6 +2,7 @@ import { Composio } from '@composio/core';
|
|
|
2
2
|
import { LangchainProvider } from '@composio/langchain';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import type { PluginTool, RuntimeContext } from '../../plugin-api/types';
|
|
5
|
+
import { withCallTimeout } from '../mcp-call-timeout';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Minimal shape of a LangChain `DynamicStructuredTool` the composio session
|
|
@@ -110,9 +111,10 @@ const refreshInFlight = new Set<string>();
|
|
|
110
111
|
|
|
111
112
|
/**
|
|
112
113
|
* Drop long-expired entries (and, if still over the cap, the
|
|
113
|
-
* soonest-to-expire ones). Runs on every cache write, so in a
|
|
114
|
-
* multi-tenant isolate one-off users' schemas are reclaimed
|
|
115
|
-
* accumulating for the isolate lifetime
|
|
114
|
+
* soonest-to-expire ones). Runs on every cache lookup and write, so in a
|
|
115
|
+
* long-running multi-tenant isolate one-off users' schemas are reclaimed
|
|
116
|
+
* instead of accumulating for the isolate lifetime, even when the replacement
|
|
117
|
+
* session open fails and no write follows. Entries get one extra TTL of grace
|
|
116
118
|
* past expiry — a recently-expired entry is still served stale while its
|
|
117
119
|
* background refresh runs, so reclaiming it immediately would put the
|
|
118
120
|
* session-open round-trip back on that user's next turn.
|
|
@@ -209,10 +211,47 @@ function normalizeArgs(toolName: string, args: unknown): unknown {
|
|
|
209
211
|
};
|
|
210
212
|
}
|
|
211
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Upper bound on one Composio tool call. The Composio SDK gives each HTTP
|
|
216
|
+
* attempt 60 s and retries a timed-out or failed attempt twice with a short
|
|
217
|
+
* back-off (about 182 s in the worst case), so this sits just past that and
|
|
218
|
+
* the SDK's own answer arrives first. What it catches is what the SDK leaves
|
|
219
|
+
* open: its timer stops once the response headers arrive, so a body that
|
|
220
|
+
* stalls after them would otherwise be waited on forever.
|
|
221
|
+
*/
|
|
222
|
+
export const COMPOSIO_TOOL_TIMEOUT_MS = 190_000;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A promise that rejects when `signal` aborts (at once if it already has),
|
|
226
|
+
* plus the call that stops listening. The SDK's LangChain tools take no
|
|
227
|
+
* signal, so the turn's cancellation is applied by racing against this.
|
|
228
|
+
*/
|
|
229
|
+
function rejectOnAbort(signal: AbortSignal): {
|
|
230
|
+
promise: Promise<never>;
|
|
231
|
+
dispose: () => void;
|
|
232
|
+
} {
|
|
233
|
+
let onAbort = (): void => undefined;
|
|
234
|
+
const promise = new Promise<never>((_, reject) => {
|
|
235
|
+
onAbort = () => {
|
|
236
|
+
const reason: unknown = signal.reason;
|
|
237
|
+
reject(reason instanceof Error ? reason : new Error(String(reason)));
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
if (signal.aborted) onAbort();
|
|
241
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
242
|
+
return {
|
|
243
|
+
promise,
|
|
244
|
+
dispose: () => signal.removeEventListener('abort', onAbort),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
212
248
|
/**
|
|
213
249
|
* Wrap a single composio session tool as a {@link PluginTool}. The handler
|
|
214
|
-
* forwards the args through the underlying `invoke
|
|
215
|
-
*
|
|
250
|
+
* forwards the args through the underlying `invoke`, bounded by
|
|
251
|
+
* {@link COMPOSIO_TOOL_TIMEOUT_MS} and the turn's abort signal (the request
|
|
252
|
+
* itself is abandoned, not cancelled: the SDK offers no way to stop it), and
|
|
253
|
+
* propagates errors so the agent surfaces a clean failure rather than a
|
|
254
|
+
* silent empty response.
|
|
216
255
|
*/
|
|
217
256
|
function wrapAsPluginTool(sessionTool: ComposioSessionTool): PluginTool {
|
|
218
257
|
return {
|
|
@@ -220,8 +259,17 @@ function wrapAsPluginTool(sessionTool: ComposioSessionTool): PluginTool {
|
|
|
220
259
|
description: sessionTool.description,
|
|
221
260
|
schema: toolSchema(sessionTool.schema),
|
|
222
261
|
handler: async (args, ctx: RuntimeContext) => {
|
|
262
|
+
const cancelled = rejectOnAbort(ctx.abortSignal);
|
|
223
263
|
try {
|
|
224
|
-
return await
|
|
264
|
+
return await withCallTimeout(
|
|
265
|
+
() =>
|
|
266
|
+
Promise.race([
|
|
267
|
+
sessionTool.invoke(normalizeArgs(sessionTool.name, args)),
|
|
268
|
+
cancelled.promise,
|
|
269
|
+
]),
|
|
270
|
+
COMPOSIO_TOOL_TIMEOUT_MS,
|
|
271
|
+
`composio tool ${sessionTool.name}`,
|
|
272
|
+
);
|
|
225
273
|
} catch (error) {
|
|
226
274
|
const detail =
|
|
227
275
|
error instanceof Error
|
|
@@ -231,6 +279,8 @@ function wrapAsPluginTool(sessionTool: ComposioSessionTool): PluginTool {
|
|
|
231
279
|
`[composio] tool "${sessionTool.name}" failed: ${detail}`,
|
|
232
280
|
);
|
|
233
281
|
throw error;
|
|
282
|
+
} finally {
|
|
283
|
+
cancelled.dispose();
|
|
234
284
|
}
|
|
235
285
|
},
|
|
236
286
|
};
|
|
@@ -307,6 +357,7 @@ export async function createComposioTools(
|
|
|
307
357
|
};
|
|
308
358
|
|
|
309
359
|
const cacheKey = defsCacheKey(opts.baseUrl, opts.userId);
|
|
360
|
+
if (opts.defsCache) pruneDefsCache(opts.defsCache, Date.now());
|
|
310
361
|
const cached = opts.defsCache?.get(cacheKey);
|
|
311
362
|
if (cached) {
|
|
312
363
|
// An expired entry is served as-is (the session meta-toolset only shifts
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RuntimeContext } from '../../plugin-api/types';
|
|
2
|
+
import { SANDBOX_CAPABILITY } from '../delegated-capabilities';
|
|
2
3
|
import { mintInvocationSafely, resolveServiceDidSafely } from '../ucan-failure';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -33,11 +34,11 @@ export async function mintComposioInvocation(
|
|
|
33
34
|
// `401 REPLAY: Invocation has already been used`. Mint fresh every call.
|
|
34
35
|
return mintInvocationSafely(
|
|
35
36
|
runCtx,
|
|
36
|
-
{ did: composioDid, capability:
|
|
37
|
+
{ did: composioDid, capability: SANDBOX_CAPABILITY.resource },
|
|
37
38
|
'composio',
|
|
38
39
|
// can: composio routes through the sandbox capability, and the user's
|
|
39
40
|
// delegation grants `sandbox/*`. A `'*'` claim is satisfiable only by a
|
|
40
41
|
// `'*'` grant — `'*'.startsWith('sandbox/')` is false.
|
|
41
|
-
{ skipCache: true, can:
|
|
42
|
+
{ skipCache: true, can: SANDBOX_CAPABILITY.action },
|
|
42
43
|
);
|
|
43
44
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ComposioSessionFactory,
|
|
12
12
|
} from './composio-tools';
|
|
13
13
|
import { mintComposioInvocation } from './composio-ucan';
|
|
14
|
+
import { SANDBOX_CAPABILITY } from '../delegated-capabilities';
|
|
14
15
|
|
|
15
16
|
const configSchema = z.object({
|
|
16
17
|
COMPOSIO_API_KEY: z.string().min(1, 'COMPOSIO_API_KEY must not be empty.'),
|
|
@@ -100,6 +101,7 @@ const manifest: PluginManifest = {
|
|
|
100
101
|
category: 'integration',
|
|
101
102
|
visibility: 'on-demand',
|
|
102
103
|
stability: 'stable',
|
|
104
|
+
requires: [SANDBOX_CAPABILITY],
|
|
103
105
|
};
|
|
104
106
|
|
|
105
107
|
export interface ComposioPluginOptions {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capabilities bundled plugins prove through the user's delegation to this
|
|
3
|
+
* oracle. Each is both the plugin's `manifest.requires` and the capability
|
|
4
|
+
* its invocations claim, so what the gate checks and what the downstream
|
|
5
|
+
* service is shown cannot drift apart. A plugin that authorizes some other
|
|
6
|
+
* way (VFS fetches its own `ixo:filesystem` delegation from the UCAN store)
|
|
7
|
+
* or degrades without a grant (skills fall back to public capsules) declares
|
|
8
|
+
* nothing.
|
|
9
|
+
*/
|
|
10
|
+
export interface DelegatedCapability {
|
|
11
|
+
readonly resource: string;
|
|
12
|
+
readonly action: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Memory Engine MCP: the memory plugin's tools. */
|
|
16
|
+
export const MEMORY_CAPABILITY: DelegatedCapability = {
|
|
17
|
+
resource: 'ixo:memory',
|
|
18
|
+
action: 'memory/*',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Sandbox MCP, and Composio, which routes through the sandbox. */
|
|
22
|
+
export const SANDBOX_CAPABILITY: DelegatedCapability = {
|
|
23
|
+
resource: 'ixo:sandbox',
|
|
24
|
+
action: 'sandbox/*',
|
|
25
|
+
};
|
|
@@ -5,17 +5,19 @@
|
|
|
5
5
|
* document. It enforces, in this order:
|
|
6
6
|
*
|
|
7
7
|
* 1. a live flow is read-only → `read_only_flow`
|
|
8
|
-
* 2.
|
|
8
|
+
* 2. an earlier write in this session failed → the same answer as step 6
|
|
9
9
|
* 3. the oracle's power level is below the room's write threshold → `needs_access`
|
|
10
10
|
* 4. the prop allowlist (the caller's `plan` step) → `prop_not_editable` etc.
|
|
11
11
|
* 5. one `doc.transact()` for the whole batch
|
|
12
|
-
* 6.
|
|
13
|
-
*
|
|
12
|
+
* 6. the provider manager's bounded `flush()` **before** success is reported:
|
|
13
|
+
* a forbidden write → `needs_access`, a definite refusal (413, 401,
|
|
14
|
+
* 400) → `error`, an unacknowledged one → `write_not_saved`
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
* mutation always "succeeds" locally, and a homeserver rejection
|
|
17
|
-
* once the batched update is actually sent. Reporting
|
|
18
|
-
* is how tools ended up claiming edits that never
|
|
16
|
+
* Step 6 exists because matrix-crdt writes are fire-and-forget: a Y.Doc
|
|
17
|
+
* mutation always "succeeds" locally, and a homeserver rejection or a failed
|
|
18
|
+
* delivery surfaces only once the batched update is actually sent. Reporting
|
|
19
|
+
* success before the flush is how tools ended up claiming edits that never
|
|
20
|
+
* landed.
|
|
19
21
|
*/
|
|
20
22
|
|
|
21
23
|
import type { MatrixClient } from 'matrix-js-sdk';
|
|
@@ -29,36 +31,40 @@ import {
|
|
|
29
31
|
} from './document-model';
|
|
30
32
|
import {
|
|
31
33
|
editorError,
|
|
32
|
-
flushTimeout,
|
|
33
34
|
isEditorFailure,
|
|
34
35
|
needsAccess,
|
|
35
36
|
readOnlyFlow,
|
|
37
|
+
writeNotSaved,
|
|
38
|
+
writeRefused,
|
|
36
39
|
type EditorFailure,
|
|
37
40
|
} from './failures';
|
|
38
41
|
import {
|
|
42
|
+
DOC_UPDATE_EVENT_TYPE,
|
|
39
43
|
MatrixProviderManager,
|
|
40
44
|
RoomNotAccessibleError,
|
|
41
45
|
type AppConfig,
|
|
46
|
+
type DocWriteFailure,
|
|
42
47
|
} from './provider';
|
|
43
48
|
|
|
44
49
|
const logger = createConsoleLogger({ module: 'editor-content-session' });
|
|
45
50
|
|
|
46
|
-
/** The Matrix event type matrix-crdt writes Y.Doc updates as. */
|
|
47
|
-
const CRDT_UPDATE_EVENT_TYPE = 'matrix-crdt.doc_update';
|
|
48
|
-
|
|
49
51
|
/** Rooms whose canonical alias starts with this hold live flows. */
|
|
50
52
|
const FLOW_ALIAS_PREFIX = '#flow-';
|
|
51
53
|
|
|
52
|
-
/** How long to wait for a write to reach the homeserver before giving up. */
|
|
53
|
-
const FLUSH_TIMEOUT_MS = 20_000;
|
|
54
|
-
|
|
55
54
|
/**
|
|
56
|
-
* What the write path needs from the
|
|
57
|
-
* it; narrowing keeps the guards testable
|
|
55
|
+
* What the write path needs from the document's provider manager.
|
|
56
|
+
* `MatrixProviderManager` satisfies it; narrowing keeps the guards testable
|
|
57
|
+
* without a Matrix connection.
|
|
58
58
|
*/
|
|
59
59
|
export interface DocumentWriter {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
/** Why an earlier write did not reach the room, if one did not. */
|
|
61
|
+
readonly writeFailure: DocWriteFailure | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Wait (bounded by the provider's own deadline and write-retry budget)
|
|
64
|
+
* until every change made so far has been sent; resolves to the failure
|
|
65
|
+
* when one did not land.
|
|
66
|
+
*/
|
|
67
|
+
flush(): Promise<DocWriteFailure | undefined>;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
/** What the write path needs from the Matrix client. */
|
|
@@ -73,7 +79,7 @@ export interface RoomStateReader {
|
|
|
73
79
|
|
|
74
80
|
export interface DocumentSession {
|
|
75
81
|
doc: Y.Doc;
|
|
76
|
-
|
|
82
|
+
writer: DocumentWriter;
|
|
77
83
|
matrixClient: RoomStateReader;
|
|
78
84
|
roomId: string;
|
|
79
85
|
/** Canonical alias, when the homeserver exposes one. */
|
|
@@ -136,18 +142,41 @@ async function resolveAlias(
|
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
|
|
145
|
+
/** Where a document lives and the client that reaches it. */
|
|
146
|
+
export interface DocumentParams {
|
|
147
|
+
matrixClient: MatrixClient;
|
|
148
|
+
appConfig: AppConfig;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** An open document plus the call that releases it. */
|
|
152
|
+
export interface OpenedDocument {
|
|
153
|
+
session: DocumentSession;
|
|
154
|
+
dispose: () => Promise<void>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Opens a document; a failure to reach it comes back typed, never thrown. */
|
|
158
|
+
export type DocumentOpener = (
|
|
159
|
+
params: DocumentParams,
|
|
160
|
+
) => Promise<OpenedDocument | EditorFailure>;
|
|
161
|
+
|
|
162
|
+
/** A thrown open/work error, as the typed failure a tool returns. */
|
|
163
|
+
function documentFailure(error: unknown, roomId: string): EditorFailure {
|
|
164
|
+
if (error instanceof RoomNotAccessibleError) {
|
|
165
|
+
return needsAccess(roomId, 'this assistant cannot open the document');
|
|
166
|
+
}
|
|
167
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
168
|
+
logger.warn(`Failed to open document ${roomId}: ${detail}`);
|
|
169
|
+
return editorError(`Could not open the document: ${detail}`, roomId);
|
|
170
|
+
}
|
|
171
|
+
|
|
139
172
|
/**
|
|
140
|
-
* Open the room's Y.Doc
|
|
141
|
-
*
|
|
142
|
-
* Access problems come back as typed failures rather than exceptions, so every
|
|
143
|
-
* tool can return them straight to the agent.
|
|
173
|
+
* Open the room's Y.Doc through matrix-crdt. On failure the provider is
|
|
174
|
+
* already disposed; on success the caller owns `dispose`.
|
|
144
175
|
*/
|
|
145
|
-
export async
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
)
|
|
149
|
-
const { matrixClient, appConfig } = params;
|
|
150
|
-
|
|
176
|
+
export const openDocumentSession: DocumentOpener = async ({
|
|
177
|
+
matrixClient,
|
|
178
|
+
appConfig,
|
|
179
|
+
}) => {
|
|
151
180
|
let roomId: string;
|
|
152
181
|
try {
|
|
153
182
|
roomId = await resolveRoomId(matrixClient, appConfig);
|
|
@@ -158,26 +187,97 @@ export async function withDocument<T>(
|
|
|
158
187
|
|
|
159
188
|
const manager = new MatrixProviderManager(matrixClient, appConfig);
|
|
160
189
|
try {
|
|
161
|
-
const { doc
|
|
190
|
+
const { doc } = await manager.init();
|
|
162
191
|
const alias = await resolveAlias(matrixClient, appConfig, roomId);
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
192
|
+
return {
|
|
193
|
+
session: {
|
|
194
|
+
doc,
|
|
195
|
+
writer: manager,
|
|
196
|
+
matrixClient,
|
|
197
|
+
roomId,
|
|
198
|
+
alias,
|
|
199
|
+
isFlow: isFlowAlias(alias),
|
|
200
|
+
},
|
|
201
|
+
dispose: () => manager.dispose(),
|
|
202
|
+
};
|
|
171
203
|
} catch (error) {
|
|
172
|
-
if (error instanceof RoomNotAccessibleError) {
|
|
173
|
-
return needsAccess(roomId, 'this assistant cannot open the document');
|
|
174
|
-
}
|
|
175
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
176
|
-
logger.warn(`Failed to open document ${roomId}: ${detail}`);
|
|
177
|
-
return editorError(`Could not open the document: ${detail}`, roomId);
|
|
178
|
-
} finally {
|
|
179
204
|
await manager.dispose();
|
|
205
|
+
return documentFailure(error, roomId);
|
|
180
206
|
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* How the content tools reach their document. `use` runs `work` against an
|
|
211
|
+
* open session; access problems and thrown errors come back as typed
|
|
212
|
+
* failures so every tool can return them straight to the agent.
|
|
213
|
+
*/
|
|
214
|
+
export interface DocumentSource {
|
|
215
|
+
use<T>(
|
|
216
|
+
work: (session: DocumentSession) => Promise<T>,
|
|
217
|
+
): Promise<T | EditorFailure>;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* One document session shared by every tool call until `close()`: opened on
|
|
222
|
+
* the first `use` (so a run that never touches the document never opens it),
|
|
223
|
+
* then reused, so a multi-step task pays the join, history walk and settle
|
|
224
|
+
* delay once. Reads see the live doc; writes still go through
|
|
225
|
+
* `applyDocumentEdit`. A failed open is not remembered — the next `use`
|
|
226
|
+
* tries again. After `close()` every `use` is refused; `close()` waits for
|
|
227
|
+
* calls still running (a write awaiting its flush) before disposing, so a
|
|
228
|
+
* cancelled run never destroys the doc under an in-flight write.
|
|
229
|
+
*/
|
|
230
|
+
export function sharedDocumentSource(
|
|
231
|
+
params: DocumentParams,
|
|
232
|
+
open: DocumentOpener = openDocumentSession,
|
|
233
|
+
): DocumentSource & { close(): Promise<void> } {
|
|
234
|
+
let opening: Promise<OpenedDocument | EditorFailure> | null = null;
|
|
235
|
+
let closed = false;
|
|
236
|
+
const inFlight = new Set<Promise<unknown>>();
|
|
237
|
+
|
|
238
|
+
const acquire = (): Promise<OpenedDocument | EditorFailure> => {
|
|
239
|
+
if (opening) return opening;
|
|
240
|
+
const attempt = open(params);
|
|
241
|
+
opening = attempt;
|
|
242
|
+
void attempt.then((result) => {
|
|
243
|
+
if (isEditorFailure(result) && opening === attempt) opening = null;
|
|
244
|
+
});
|
|
245
|
+
return attempt;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const run = async <T>(
|
|
249
|
+
work: (session: DocumentSession) => Promise<T>,
|
|
250
|
+
): Promise<T | EditorFailure> => {
|
|
251
|
+
const opened = await acquire();
|
|
252
|
+
if (isEditorFailure(opened)) return opened;
|
|
253
|
+
try {
|
|
254
|
+
return await work(opened.session);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
return documentFailure(error, opened.session.roomId);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
async use(work) {
|
|
262
|
+
if (closed) return editorError('The document session has ended.');
|
|
263
|
+
const call = run(work);
|
|
264
|
+
inFlight.add(call);
|
|
265
|
+
try {
|
|
266
|
+
return await call;
|
|
267
|
+
} finally {
|
|
268
|
+
inFlight.delete(call);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
async close() {
|
|
272
|
+
closed = true;
|
|
273
|
+
await Promise.allSettled([...inFlight]);
|
|
274
|
+
const pending = opening;
|
|
275
|
+
opening = null;
|
|
276
|
+
if (!pending) return;
|
|
277
|
+
const opened = await pending;
|
|
278
|
+
if (!isEditorFailure(opened)) await opened.dispose();
|
|
279
|
+
},
|
|
280
|
+
};
|
|
181
281
|
}
|
|
182
282
|
|
|
183
283
|
/** A live flow's room alias is `#flow-…`; a page's is `#page-…`. */
|
|
@@ -223,7 +323,7 @@ export async function canOracleWrite(
|
|
|
223
323
|
const eventsDefault = numberAt(content, 'events_default', 0);
|
|
224
324
|
const required = numberAt(
|
|
225
325
|
content.events,
|
|
226
|
-
|
|
326
|
+
DOC_UPDATE_EVENT_TYPE,
|
|
227
327
|
eventsDefault,
|
|
228
328
|
);
|
|
229
329
|
|
|
@@ -231,34 +331,20 @@ export async function canOracleWrite(
|
|
|
231
331
|
}
|
|
232
332
|
|
|
233
333
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
334
|
+
* The tool result for a write that did not land: the homeserver forbidding it
|
|
335
|
+
* means the assistant lacks access; a definite refusal means nothing was
|
|
336
|
+
* stored; otherwise delivery is uncertain.
|
|
237
337
|
*/
|
|
238
|
-
function
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
});
|
|
249
|
-
try {
|
|
250
|
-
return await Promise.race([
|
|
251
|
-
provider.waitForFlush().then((): 'flushed' => 'flushed'),
|
|
252
|
-
timeout,
|
|
253
|
-
]);
|
|
254
|
-
} catch (error) {
|
|
255
|
-
logger.warn(
|
|
256
|
-
`waitForFlush rejected: ${error instanceof Error ? error.message : String(error)}`,
|
|
257
|
-
);
|
|
258
|
-
return 'error';
|
|
259
|
-
} finally {
|
|
260
|
-
if (timer) clearTimeout(timer);
|
|
261
|
-
}
|
|
338
|
+
function writeFailureResult(
|
|
339
|
+
failure: DocWriteFailure,
|
|
340
|
+
roomId: string,
|
|
341
|
+
forbiddenDetail: string,
|
|
342
|
+
): EditorFailure {
|
|
343
|
+
if (failure.kind === 'forbidden') return needsAccess(roomId, forbiddenDetail);
|
|
344
|
+
logger.warn(`Document write to ${roomId} not delivered: ${failure.detail}`);
|
|
345
|
+
return failure.mayHaveLanded
|
|
346
|
+
? writeNotSaved(roomId, failure.detail)
|
|
347
|
+
: writeRefused(roomId, failure.detail);
|
|
262
348
|
}
|
|
263
349
|
|
|
264
350
|
/**
|
|
@@ -283,8 +369,12 @@ export async function applyDocumentEdit<TPlan, TResult>(
|
|
|
283
369
|
return readOnlyFlow(session.roomId, session.alias);
|
|
284
370
|
}
|
|
285
371
|
|
|
286
|
-
|
|
287
|
-
|
|
372
|
+
// Once a write in this session has failed, the provider refuses every
|
|
373
|
+
// further one, so report that failure instead of attempting another.
|
|
374
|
+
const earlier = session.writer.writeFailure;
|
|
375
|
+
if (earlier) {
|
|
376
|
+
return writeFailureResult(
|
|
377
|
+
earlier,
|
|
288
378
|
session.roomId,
|
|
289
379
|
'a previous write to this document was rejected',
|
|
290
380
|
);
|
|
@@ -313,15 +403,15 @@ export async function applyDocumentEdit<TPlan, TResult>(
|
|
|
313
403
|
if (titleSeed) seedDocumentTitle(session.doc, titleSeed);
|
|
314
404
|
}, EDIT_ORIGIN);
|
|
315
405
|
|
|
316
|
-
|
|
317
|
-
if (flush !== 'flushed') {
|
|
318
|
-
return flushTimeout(session.roomId);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// The homeserver's verdict only arrives with the flush; a rejection here
|
|
406
|
+
// The homeserver's verdict only arrives with the flush; a failure here
|
|
322
407
|
// means the mutation exists locally but never reached the room.
|
|
323
|
-
|
|
324
|
-
|
|
408
|
+
const failure = await session.writer.flush();
|
|
409
|
+
if (failure) {
|
|
410
|
+
return writeFailureResult(
|
|
411
|
+
failure,
|
|
412
|
+
session.roomId,
|
|
413
|
+
'the homeserver rejected the write',
|
|
414
|
+
);
|
|
325
415
|
}
|
|
326
416
|
|
|
327
417
|
if (result === undefined) {
|