@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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { tool } from '../../plugin-api/tool-helper';
|
|
3
|
+
import type {
|
|
4
|
+
PluginSubAgent,
|
|
5
|
+
PluginTool,
|
|
6
|
+
RuntimeContext,
|
|
7
|
+
} from '../../plugin-api/types';
|
|
8
|
+
import type { BlueprintSection } from './blueprint-types';
|
|
9
|
+
import type { BlueprintStore, BlueprintStoreFor } from './blueprint-store';
|
|
10
|
+
import type { CapsuleContentClient } from './capsule-content-client';
|
|
11
|
+
import {
|
|
12
|
+
DESIGN_POD_ROLES,
|
|
13
|
+
type DesignPodRole,
|
|
14
|
+
type DesignPodStage,
|
|
15
|
+
} from './design-pod-roles';
|
|
16
|
+
import { deriveStage, sectionsView } from './stage';
|
|
17
|
+
|
|
18
|
+
const isGateRole = (role: DesignPodRole): boolean =>
|
|
19
|
+
role.stage === 'evaluate' || role.stage === 'gate';
|
|
20
|
+
|
|
21
|
+
/** What each lifecycle stage is accountable for, for the fallback prompts. */
|
|
22
|
+
const STAGE_DUTIES: Record<DesignPodStage, string> = {
|
|
23
|
+
qualify:
|
|
24
|
+
'Score the incoming service intent for viability and fit, and give a clear go / no-go rationale the later stages can rely on.',
|
|
25
|
+
architect:
|
|
26
|
+
'Design the POD structure for your discipline — service roles, workspaces and services, claim schemas and the UDID model, or the UCAN rights model — precisely enough for the build stage to implement without guessing.',
|
|
27
|
+
build:
|
|
28
|
+
'Produce the executable artifacts for your discipline — Flow pages for the workflow and UX, or the operating playbooks and rule cards — grounded in the architect sections.',
|
|
29
|
+
evaluate:
|
|
30
|
+
'Judge the recorded design from your lens (automation feasibility, governance and risk, or the outcome contract). Be adversarial: name what breaks, and fail the gate when the blueprint does not hold up.',
|
|
31
|
+
package:
|
|
32
|
+
'Package the POD for the market — the commercial offer and marketplace listing draft, or a runnable demo — from the completed design sections.',
|
|
33
|
+
gate: 'Run final QA across every prior section and issue the launch-readiness verdict. Pass only a blueprint you would stand behind going on-chain.',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function sectionInstruction(role: DesignPodRole): string {
|
|
37
|
+
const verdict = isGateRole(role)
|
|
38
|
+
? ' Include a pass|fail verdict (and blockers when failing).'
|
|
39
|
+
: '';
|
|
40
|
+
return `Use read_blueprint for the sections recorded so far (pass roles=[...] when you need a section's full content), then call submit_section exactly once with your section content.${verdict}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The role's registry instructions, framed for the pipeline. */
|
|
44
|
+
function composePrompt(role: DesignPodRole, skillMarkdown: string): string {
|
|
45
|
+
return `${skillMarkdown}\n\n---\nYou are the \`${role.id}\` specialist in the POD design pipeline (stage: ${role.stage}). ${sectionInstruction(role)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Built-in prompt used when no registry instructions are loaded for the role. */
|
|
49
|
+
function fallbackPrompt(role: DesignPodRole): string {
|
|
50
|
+
return [
|
|
51
|
+
`You are the \`${role.id}\` specialist in the POD design pipeline (stage: ${role.stage}).`,
|
|
52
|
+
role.description,
|
|
53
|
+
'',
|
|
54
|
+
STAGE_DUTIES[role.stage],
|
|
55
|
+
'Ground everything in the POD brief and the sections recorded before yours; produce concrete, structured content — not commentary.',
|
|
56
|
+
'',
|
|
57
|
+
sectionInstruction(role),
|
|
58
|
+
'',
|
|
59
|
+
'(Operating from the built-in summary of this role; its registry-published instructions are not loaded.)',
|
|
60
|
+
].join('\n');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const submitSchema = z.object({
|
|
64
|
+
content: z
|
|
65
|
+
.unknown()
|
|
66
|
+
.describe('Your section content to record into the POD blueprint.'),
|
|
67
|
+
verdict: z
|
|
68
|
+
.enum(['pass', 'fail'])
|
|
69
|
+
.optional()
|
|
70
|
+
.describe('For gate roles, your gate verdict.'),
|
|
71
|
+
blockers: z
|
|
72
|
+
.array(z.string())
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Blocking issues to surface when your verdict is fail.'),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const readSchema = z.object({
|
|
78
|
+
roles: z
|
|
79
|
+
.array(z.string())
|
|
80
|
+
.optional()
|
|
81
|
+
.describe(
|
|
82
|
+
'Role ids whose full section content to include. Omit for the compact per-section summary.',
|
|
83
|
+
),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** The narrow tool set each specialist gets: read prior context, submit once. */
|
|
87
|
+
function roleTools(
|
|
88
|
+
role: DesignPodRole,
|
|
89
|
+
storeFor: BlueprintStoreFor,
|
|
90
|
+
): PluginTool[] {
|
|
91
|
+
const readBlueprint = tool(
|
|
92
|
+
async (args, ctx) => {
|
|
93
|
+
const { roles } = readSchema.parse(args);
|
|
94
|
+
const bp = await storeFor(ctx).get(ctx.session.id);
|
|
95
|
+
if (!bp) {
|
|
96
|
+
return { brief: undefined, sections: [] };
|
|
97
|
+
}
|
|
98
|
+
return { brief: bp.brief, ...sectionsView(bp, roles) };
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'read_blueprint',
|
|
102
|
+
description:
|
|
103
|
+
'Read the POD brief and a compact summary of the sections recorded so far. Pass roles=[...] to include the full content of specific sections.',
|
|
104
|
+
schema: readSchema,
|
|
105
|
+
effect: 'read',
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const submitSection = tool(
|
|
110
|
+
async (args, ctx) => {
|
|
111
|
+
const input = submitSchema.parse(args);
|
|
112
|
+
const section: BlueprintSection = {
|
|
113
|
+
role: role.id,
|
|
114
|
+
stage: role.stage,
|
|
115
|
+
content: input.content,
|
|
116
|
+
recordedAt: new Date().toISOString(),
|
|
117
|
+
...(input.verdict !== undefined ? { verdict: input.verdict } : {}),
|
|
118
|
+
...(input.blockers !== undefined ? { blockers: input.blockers } : {}),
|
|
119
|
+
};
|
|
120
|
+
await storeFor(ctx).putSection(ctx.session.id, section);
|
|
121
|
+
return { submitted: role.id, stage: role.stage };
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'submit_section',
|
|
125
|
+
description: `Record your (${role.id}) section into the POD blueprint. Call exactly once.`,
|
|
126
|
+
schema: submitSchema,
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return [readBlueprint, submitSection];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function resolvePrompt(
|
|
134
|
+
role: DesignPodRole,
|
|
135
|
+
rt: RuntimeContext,
|
|
136
|
+
capsules: CapsuleContentClient,
|
|
137
|
+
): Promise<string> {
|
|
138
|
+
const markdown = await capsules.getSkillMarkdown(role.capsule, rt);
|
|
139
|
+
return markdown === undefined
|
|
140
|
+
? fallbackPrompt(role)
|
|
141
|
+
: composePrompt(role, markdown);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function buildSubAgent(
|
|
145
|
+
role: DesignPodRole,
|
|
146
|
+
rt: RuntimeContext,
|
|
147
|
+
storeFor: BlueprintStoreFor,
|
|
148
|
+
capsules: CapsuleContentClient,
|
|
149
|
+
): Promise<PluginSubAgent> {
|
|
150
|
+
return {
|
|
151
|
+
name: role.id,
|
|
152
|
+
description: role.description,
|
|
153
|
+
systemPrompt: await resolvePrompt(role, rt, capsules),
|
|
154
|
+
tools: roleTools(role, storeFor),
|
|
155
|
+
model: 'subagent',
|
|
156
|
+
forwardTools: true,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build the specialist sub-agents for the current request, gated to the
|
|
162
|
+
* blueprint's current stage. Returns only that stage's specialist(s) — never
|
|
163
|
+
* all twelve — so the conductor's tool surface stays small and the pipeline
|
|
164
|
+
* order is enforced, and none at all before a blueprint exists. Each
|
|
165
|
+
* sub-agent's system prompt is the role's registry `SKILL.md`, fetched via the
|
|
166
|
+
* capsule client, with a built-in fallback when there is no fetcher or the
|
|
167
|
+
* registry is unavailable.
|
|
168
|
+
*
|
|
169
|
+
* The specialists' tools resolve the store from THEIR call's context (the
|
|
170
|
+
* sub-agent runs later in the turn), never from the `rt` that built them.
|
|
171
|
+
*/
|
|
172
|
+
export async function buildStageSubAgents(
|
|
173
|
+
rt: RuntimeContext,
|
|
174
|
+
storeFor: BlueprintStoreFor,
|
|
175
|
+
capsules: CapsuleContentClient,
|
|
176
|
+
): Promise<PluginSubAgent[]> {
|
|
177
|
+
const store: BlueprintStore = storeFor(rt);
|
|
178
|
+
const bp = await store.get(rt.session.id);
|
|
179
|
+
// No design in this thread: nothing to offer. This hook runs on every turn
|
|
180
|
+
// of every user whether or not the capability is loaded, so it must stay a
|
|
181
|
+
// single local read until `start_pod_design` has opened a blueprint (the
|
|
182
|
+
// qualify specialist appears on the following turn).
|
|
183
|
+
if (!bp) return [];
|
|
184
|
+
const stage = deriveStage(bp);
|
|
185
|
+
const roles = DESIGN_POD_ROLES.filter((role) => role.stage === stage);
|
|
186
|
+
return Promise.all(
|
|
187
|
+
roles.map((role) => buildSubAgent(role, rt, storeFor, capsules)),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Node runtime's `logActionToMatrix`: a best-effort `ixo.action.log`
|
|
3
3
|
* room event recording a frontend action (browser tool / AG-UI action) and
|
|
4
|
-
* its outcome, posted only when the turn has a room. Never throws.
|
|
4
|
+
* its outcome, posted only when the turn has a room. Never throws. Frontend
|
|
5
|
+
* calls log through `logFrontendAction`, which keeps their content out.
|
|
5
6
|
*
|
|
6
7
|
* Retried across a gateway restart (the gateway object is replaced on every
|
|
7
8
|
* deploy and can be drained mid-turn) and handed to the host to keep alive
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
* response lost between the homeserver's ack and the reply to the gateway
|
|
11
12
|
* is deduplicated instead of logged twice.
|
|
12
13
|
*/
|
|
14
|
+
import { summarizeFrontendResult } from '@ixo/common/ai/frontend-bridge';
|
|
13
15
|
import { retryGateway, type RetryGatewayOptions } from '../../do/gateway-retry';
|
|
14
16
|
import { retryTxnId } from '../../matrix/txn-id';
|
|
15
17
|
import type { RuntimeContext } from '../../plugin-api/types';
|
|
@@ -59,3 +61,21 @@ export function logActionToMatrix(
|
|
|
59
61
|
);
|
|
60
62
|
ctx.background?.(work);
|
|
61
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Log a finished frontend call (browser tool / AG-UI action). The room event
|
|
67
|
+
* is a diagnostic trail, readable by everyone in the room: it records the
|
|
68
|
+
* invocation id, a Portal command id when the result carries one, and
|
|
69
|
+
* whether the outcome is unknown — never the arguments or the result body,
|
|
70
|
+
* which carry the user's content.
|
|
71
|
+
*/
|
|
72
|
+
export function logFrontendAction(
|
|
73
|
+
ctx: Pick<RuntimeContext, 'matrix' | 'session' | 'logger' | 'background'>,
|
|
74
|
+
call: { name: string; invocationId: string | undefined; result: unknown },
|
|
75
|
+
): void {
|
|
76
|
+
logActionToMatrix(ctx, {
|
|
77
|
+
name: call.name,
|
|
78
|
+
args: {},
|
|
79
|
+
...summarizeFrontendResult(call.result, call.invocationId),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation for the tools a browser declares in a turn body (the Portal's
|
|
3
|
+
* `tools[]` and AG-UI's `agActions[]`). The declarations come from the
|
|
4
|
+
* client on every request, so each request is checked on its own: the boot
|
|
5
|
+
* time collision check never sees them.
|
|
6
|
+
*
|
|
7
|
+
* An entry is dropped (never repaired) when its name is not a valid tool
|
|
8
|
+
* name, repeats an earlier entry's name, or is reserved (`reservedNames`,
|
|
9
|
+
* e.g. the server's own tool names), or when its description or schema is
|
|
10
|
+
* over the size cap. At most {@link MAX_DECLARED_TOOLS} entries are kept.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Tool names the model providers accept, and nothing else. */
|
|
14
|
+
export const DECLARED_TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
15
|
+
/** Declared tools kept per request; further entries are dropped. */
|
|
16
|
+
export const MAX_DECLARED_TOOLS = 64;
|
|
17
|
+
/** Characters of one declared tool's description. */
|
|
18
|
+
export const MAX_DECLARED_DESCRIPTION_CHARS = 2_048;
|
|
19
|
+
/** Characters of one declared tool's JSON schema, serialised. */
|
|
20
|
+
export const MAX_DECLARED_SCHEMA_CHARS = 16_384;
|
|
21
|
+
|
|
22
|
+
export interface DeclaredTool {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
schema: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DroppedDeclaration {
|
|
29
|
+
name: string;
|
|
30
|
+
reason: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function schemaLength(schema: Record<string, unknown>): number {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.stringify(schema).length;
|
|
36
|
+
} catch {
|
|
37
|
+
return Number.POSITIVE_INFINITY;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The entries that may become tools this turn, plus why the others did not. */
|
|
42
|
+
export function sanitizeDeclaredTools<T extends DeclaredTool>(
|
|
43
|
+
entries: readonly T[],
|
|
44
|
+
reservedNames: ReadonlySet<string> = new Set(),
|
|
45
|
+
): { kept: T[]; dropped: DroppedDeclaration[] } {
|
|
46
|
+
const kept: T[] = [];
|
|
47
|
+
const dropped: DroppedDeclaration[] = [];
|
|
48
|
+
const seen = new Set<string>();
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const { name } = entry;
|
|
51
|
+
let reason: string | undefined;
|
|
52
|
+
if (!DECLARED_TOOL_NAME_PATTERN.test(name)) reason = 'invalid name';
|
|
53
|
+
else if (seen.has(name)) reason = 'duplicate name';
|
|
54
|
+
else if (reservedNames.has(name)) reason = 'name taken by a server tool';
|
|
55
|
+
else if (entry.description.length > MAX_DECLARED_DESCRIPTION_CHARS)
|
|
56
|
+
reason = `description over ${MAX_DECLARED_DESCRIPTION_CHARS} characters`;
|
|
57
|
+
else if (schemaLength(entry.schema) > MAX_DECLARED_SCHEMA_CHARS)
|
|
58
|
+
reason = `schema over ${MAX_DECLARED_SCHEMA_CHARS} characters`;
|
|
59
|
+
else if (kept.length >= MAX_DECLARED_TOOLS)
|
|
60
|
+
reason = `more than ${MAX_DECLARED_TOOLS} tools declared`;
|
|
61
|
+
if (DECLARED_TOOL_NAME_PATTERN.test(name)) seen.add(name);
|
|
62
|
+
if (reason) dropped.push({ name: name.slice(0, 64), reason });
|
|
63
|
+
else kept.push(entry);
|
|
64
|
+
}
|
|
65
|
+
return { kept, dropped };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Dropped entries named in {@link describeDropped}; the rest are counted. */
|
|
69
|
+
const MAX_DROPPED_NAMED = 5;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One log line for a request's dropped declarations (`undefined` when none
|
|
73
|
+
* were dropped): the count plus the first few names and reasons, so a client
|
|
74
|
+
* declaring thousands of bad entries still costs one bounded line.
|
|
75
|
+
*/
|
|
76
|
+
export function describeDropped(
|
|
77
|
+
kind: string,
|
|
78
|
+
dropped: readonly DroppedDeclaration[],
|
|
79
|
+
): string | undefined {
|
|
80
|
+
if (dropped.length === 0) return undefined;
|
|
81
|
+
const named = dropped
|
|
82
|
+
.slice(0, MAX_DROPPED_NAMED)
|
|
83
|
+
.map(({ name, reason }) => `"${name}" (${reason})`)
|
|
84
|
+
.join(', ');
|
|
85
|
+
const more =
|
|
86
|
+
dropped.length > MAX_DROPPED_NAMED
|
|
87
|
+
? ` and ${dropped.length - MAX_DROPPED_NAMED} more`
|
|
88
|
+
: '';
|
|
89
|
+
return `Ignoring ${dropped.length} declared ${kind}: ${named}${more}`;
|
|
90
|
+
}
|
|
@@ -23,7 +23,8 @@ import type {
|
|
|
23
23
|
PluginTool,
|
|
24
24
|
RuntimeContext,
|
|
25
25
|
} from '../../plugin-api/types';
|
|
26
|
-
import {
|
|
26
|
+
import { logFrontendAction } from './action-log';
|
|
27
|
+
import { describeDropped, sanitizeDeclaredTools } from './declared-tools';
|
|
27
28
|
|
|
28
29
|
const manifest: PluginManifest = {
|
|
29
30
|
title: 'Portal',
|
|
@@ -53,6 +54,20 @@ const manifest: PluginManifest = {
|
|
|
53
54
|
|
|
54
55
|
export const BROWSER_TOOL_TIMEOUT_MS = 15_000;
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The Portal's Topic write tool. Its commands can take a while to settle in
|
|
59
|
+
* the browser (several Matrix writes), so it gets a longer window before the
|
|
60
|
+
* call is reported as an unknown outcome.
|
|
61
|
+
*/
|
|
62
|
+
export const TOPIC_MUTATION_TOOL = 'mutate_topic';
|
|
63
|
+
export const TOPIC_MUTATION_TIMEOUT_MS = 120_000;
|
|
64
|
+
|
|
65
|
+
export function browserToolTimeoutMs(toolName: string): number {
|
|
66
|
+
return toolName === TOPIC_MUTATION_TOOL
|
|
67
|
+
? TOPIC_MUTATION_TIMEOUT_MS
|
|
68
|
+
: BROWSER_TOOL_TIMEOUT_MS;
|
|
69
|
+
}
|
|
70
|
+
|
|
56
71
|
const BROWSER_TOOL_SHAPE = z.object({
|
|
57
72
|
name: z.string(),
|
|
58
73
|
description: z.string(),
|
|
@@ -61,12 +76,16 @@ const BROWSER_TOOL_SHAPE = z.object({
|
|
|
61
76
|
|
|
62
77
|
const ARGS_RECORD_SHAPE = z.record(z.string(), z.unknown());
|
|
63
78
|
|
|
79
|
+
/** The client's browser tools; a name declared twice keeps its first descriptor. */
|
|
64
80
|
export function parseBrowserTools(value: unknown): BrowserToolCall[] {
|
|
65
81
|
if (!Array.isArray(value)) return [];
|
|
66
82
|
const out: BrowserToolCall[] = [];
|
|
83
|
+
const names = new Set<string>();
|
|
67
84
|
for (const entry of value) {
|
|
68
85
|
const parsed = BROWSER_TOOL_SHAPE.safeParse(entry);
|
|
69
|
-
if (parsed.success
|
|
86
|
+
if (!parsed.success || names.has(parsed.data.name)) continue;
|
|
87
|
+
names.add(parsed.data.name);
|
|
88
|
+
out.push(parsed.data);
|
|
70
89
|
}
|
|
71
90
|
return out;
|
|
72
91
|
}
|
|
@@ -154,36 +173,43 @@ export function buildBrowserTool(
|
|
|
154
173
|
descriptor,
|
|
155
174
|
ctx.config,
|
|
156
175
|
);
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
const toolCallId = `tc-${ctx.session.requestId || 'noreq'}-${crypto
|
|
160
|
-
.randomUUID()
|
|
161
|
-
.slice(0, 8)}`;
|
|
176
|
+
// The bridge makes every invocation unique (`tc-<requestId>:<uuid>`).
|
|
177
|
+
let invocationId: string | undefined;
|
|
162
178
|
const result = await frontend.callBrowserTool({
|
|
163
179
|
sessionId,
|
|
164
|
-
toolCallId
|
|
180
|
+
toolCallId: `tc-${ctx.session.requestId || 'noreq'}`,
|
|
165
181
|
toolName: descriptor.name,
|
|
166
182
|
args,
|
|
167
|
-
timeoutMs:
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
result,
|
|
173
|
-
success: true,
|
|
183
|
+
timeoutMs: browserToolTimeoutMs(descriptor.name),
|
|
184
|
+
signal: ctx.abortSignal,
|
|
185
|
+
onInvocation: (id) => {
|
|
186
|
+
invocationId = id;
|
|
187
|
+
},
|
|
174
188
|
});
|
|
189
|
+
logFrontendAction(ctx, { name: descriptor.name, invocationId, result });
|
|
175
190
|
return result;
|
|
176
191
|
},
|
|
177
192
|
{
|
|
178
193
|
name: descriptor.name,
|
|
179
194
|
description: descriptor.description,
|
|
180
195
|
schema,
|
|
196
|
+
// A browser step (scroll, page, click) repeated is a new step.
|
|
197
|
+
repeatable: true,
|
|
181
198
|
},
|
|
182
199
|
);
|
|
183
200
|
}
|
|
184
201
|
|
|
202
|
+
/**
|
|
203
|
+
* The browser tools declared on this request that may become tools: invalid,
|
|
204
|
+
* duplicate and oversized declarations are dropped (`declared-tools.ts`).
|
|
205
|
+
*/
|
|
185
206
|
function readBrowserTools(rtCtx: RuntimeContext): BrowserToolCall[] {
|
|
186
|
-
|
|
207
|
+
const { kept, dropped } = sanitizeDeclaredTools(
|
|
208
|
+
parseBrowserTools(rtCtx.history.state.browserTools),
|
|
209
|
+
);
|
|
210
|
+
const summary = describeDropped('browser tools', dropped);
|
|
211
|
+
if (summary) rtCtx.logger.warn(summary);
|
|
212
|
+
return kept;
|
|
187
213
|
}
|
|
188
214
|
|
|
189
215
|
export class PortalPlugin extends OraclePlugin {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MultiServerMCPClient } from '@langchain/mcp-adapters';
|
|
2
2
|
import type { RuntimeContext } from '../../plugin-api/types';
|
|
3
3
|
import { base64ToBytes } from '../base64';
|
|
4
|
+
import { withCallTimeout } from '../mcp-call-timeout';
|
|
4
5
|
import { adaptMcpClientTools } from '../mcp-tool-adapter';
|
|
5
6
|
import { createDefaultAuthBuilder, SANDBOX_RUN_TOOL_NAME } from './sandbox-mcp';
|
|
6
7
|
import type {
|
|
@@ -12,8 +13,21 @@ import type {
|
|
|
12
13
|
/** Upstream tool name the sandbox MCP surfaces for byte-perfect file writes. */
|
|
13
14
|
export const SANDBOX_WRITE_FILE_TOOL_NAME = 'sandbox_write_file';
|
|
14
15
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Per-call timeout for every sandbox MCP tool call. It is the MCP request's
|
|
18
|
+
* own timeout (as `metadata.timeoutMs`, see `mcp-tool-adapter.ts`) and is
|
|
19
|
+
* also enforced with {@link withCallTimeout}, which closes the client on
|
|
20
|
+
* expiry; never as the client's `defaultToolTimeout`.
|
|
21
|
+
*/
|
|
22
|
+
export const SANDBOX_MCP_TIMEOUT_MS = 180_000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Largest file the bridge moves in either direction. A transfer is buffered
|
|
26
|
+
* whole in the isolate several times over (base64 text in the MCP result,
|
|
27
|
+
* its decoded bytes, the upload copy), so the cap keeps one transfer far
|
|
28
|
+
* below the 128 MB isolate limit. Equal to the VFS vision-read cap.
|
|
29
|
+
*/
|
|
30
|
+
export const MAX_SANDBOX_TRANSFER_BYTES = 10 * 1024 * 1024;
|
|
17
31
|
|
|
18
32
|
/** Only destination the sandbox accepts for `sandbox_write_file` writes. */
|
|
19
33
|
export const WORKSPACE_DATA_PREFIX = '/workspace/data/';
|
|
@@ -21,6 +35,12 @@ export const WORKSPACE_DATA_PREFIX = '/workspace/data/';
|
|
|
21
35
|
/** Sentinel echoed by the read command when the source file is absent. */
|
|
22
36
|
export const SANDBOX_NO_FILE_SENTINEL = '__SANDBOX_NOFILE__';
|
|
23
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Sentinel echoed by the read command, followed by the size in bytes, when
|
|
40
|
+
* the source file exceeds {@link MAX_SANDBOX_TRANSFER_BYTES}.
|
|
41
|
+
*/
|
|
42
|
+
export const SANDBOX_TOO_LARGE_SENTINEL = '__SANDBOX_TOOLARGE__';
|
|
43
|
+
|
|
24
44
|
/**
|
|
25
45
|
* Shown when the user hasn't authorized the oracle to use the sandbox — the
|
|
26
46
|
* default auth builder can't mint an `ixo:sandbox` invocation, so there's no
|
|
@@ -41,7 +61,10 @@ export const defaultSandboxMcpClientFactory: SandboxMcpClientFactory = (
|
|
|
41
61
|
) => {
|
|
42
62
|
const client = new MultiServerMCPClient(config);
|
|
43
63
|
const wrapper: SandboxMcpClientLike = {
|
|
44
|
-
getTools: async () =>
|
|
64
|
+
getTools: async () =>
|
|
65
|
+
adaptMcpClientTools(await client.getTools(), console, {
|
|
66
|
+
requestTimeoutMs: SANDBOX_MCP_TIMEOUT_MS,
|
|
67
|
+
}),
|
|
45
68
|
close: () => client.close(),
|
|
46
69
|
};
|
|
47
70
|
return wrapper;
|
|
@@ -201,7 +224,6 @@ export async function getSandboxBridge(
|
|
|
201
224
|
headers,
|
|
202
225
|
},
|
|
203
226
|
},
|
|
204
|
-
defaultToolTimeout: SANDBOX_MCP_TIMEOUT_MS,
|
|
205
227
|
useStandardContentBlocks: true,
|
|
206
228
|
});
|
|
207
229
|
|
|
@@ -236,23 +258,57 @@ export async function getSandboxBridge(
|
|
|
236
258
|
};
|
|
237
259
|
}
|
|
238
260
|
|
|
239
|
-
|
|
261
|
+
// Each invocation is bounded by a timer that is always cleared; on timeout
|
|
262
|
+
// the client is closed so the abandoned request cannot keep the object
|
|
263
|
+
// busy.
|
|
264
|
+
const bounded = (t: SandboxMcpTool): SandboxMcpTool => ({
|
|
265
|
+
...t,
|
|
266
|
+
invoke: (input: unknown) =>
|
|
267
|
+
withCallTimeout(
|
|
268
|
+
() => t.invoke(input),
|
|
269
|
+
SANDBOX_MCP_TIMEOUT_MS,
|
|
270
|
+
`sandbox tool ${t.name}`,
|
|
271
|
+
close,
|
|
272
|
+
),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return { run: bounded(run), writeFile: bounded(writeFile), close };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Shell command that prints the file as one base64 line, or a sentinel when
|
|
280
|
+
* it is missing or larger than {@link MAX_SANDBOX_TRANSFER_BYTES}. The size
|
|
281
|
+
* is checked inside the sandbox so an oversized file is never encoded or
|
|
282
|
+
* sent. `sandboxPath` must already have passed {@link hasShellUnsafeChars}.
|
|
283
|
+
*/
|
|
284
|
+
export function sandboxReadCommand(sandboxPath: string): string {
|
|
285
|
+
const p = `'${sandboxPath}'`;
|
|
286
|
+
return [
|
|
287
|
+
`if [ ! -f ${p} ]; then echo ${SANDBOX_NO_FILE_SENTINEL};`,
|
|
288
|
+
`else s=$(stat -c%s ${p});`,
|
|
289
|
+
`if [ "$s" -gt ${MAX_SANDBOX_TRANSFER_BYTES} ]; then echo ${SANDBOX_TOO_LARGE_SENTINEL}:$s;`,
|
|
290
|
+
`else base64 -w0 ${p}; fi; fi`,
|
|
291
|
+
].join(' ');
|
|
240
292
|
}
|
|
241
293
|
|
|
242
294
|
/**
|
|
243
295
|
* Read a file out of the sandbox as bytes, base64-hopping over `sandbox_run`
|
|
244
296
|
* so binary content survives the transport. Non-throwing — a missing file, a
|
|
245
|
-
*
|
|
246
|
-
* agent-facing
|
|
297
|
+
* file over {@link MAX_SANDBOX_TRANSFER_BYTES}, a failed command, and
|
|
298
|
+
* undecodable output all come back as `{ error }` with an agent-facing
|
|
299
|
+
* message.
|
|
247
300
|
*/
|
|
248
301
|
export async function readSandboxFile(
|
|
249
302
|
bridge: Pick<SandboxBridge, 'run'>,
|
|
250
303
|
sandboxPath: string,
|
|
251
304
|
): Promise<{ bytes: Uint8Array } | { error: string }> {
|
|
305
|
+
if (hasShellUnsafeChars(sandboxPath)) {
|
|
306
|
+
return {
|
|
307
|
+
error: `The sandbox path contains characters I can't safely handle (quotes or newlines).`,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
252
310
|
const read = readSandboxResult(
|
|
253
|
-
await bridge.run.invoke({
|
|
254
|
-
code: `test -f '${sandboxPath}' && base64 -w0 '${sandboxPath}' || echo ${SANDBOX_NO_FILE_SENTINEL}`,
|
|
255
|
-
}),
|
|
311
|
+
await bridge.run.invoke({ code: sandboxReadCommand(sandboxPath) }),
|
|
256
312
|
);
|
|
257
313
|
if (!read.ok) {
|
|
258
314
|
return {
|
|
@@ -263,6 +319,19 @@ export async function readSandboxFile(
|
|
|
263
319
|
if (stdout === SANDBOX_NO_FILE_SENTINEL) {
|
|
264
320
|
return { error: `No file at \`${sandboxPath}\` in the sandbox.` };
|
|
265
321
|
}
|
|
322
|
+
if (stdout.startsWith(`${SANDBOX_TOO_LARGE_SENTINEL}:`)) {
|
|
323
|
+
const size = stdout.slice(SANDBOX_TOO_LARGE_SENTINEL.length + 1);
|
|
324
|
+
return {
|
|
325
|
+
error: `\`${sandboxPath}\` is ${size} bytes; files moved out of the sandbox are limited to ${MAX_SANDBOX_TRANSFER_BYTES} bytes.`,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
// Defence in depth for a sandbox that ignored the size check: base64
|
|
329
|
+
// inflates by 4/3, so anything longer decodes past the cap.
|
|
330
|
+
if (stdout.length > Math.ceil(MAX_SANDBOX_TRANSFER_BYTES / 3) * 4) {
|
|
331
|
+
return {
|
|
332
|
+
error: `\`${sandboxPath}\` is larger than the ${MAX_SANDBOX_TRANSFER_BYTES}-byte limit for files moved out of the sandbox.`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
266
335
|
try {
|
|
267
336
|
return { bytes: base64ToBytes(stdout) };
|
|
268
337
|
} catch {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RuntimeContext } from '../../plugin-api/types';
|
|
2
|
+
import { SANDBOX_CAPABILITY } from '../delegated-capabilities';
|
|
2
3
|
|
|
3
4
|
/** Tool name surfaced by the upstream sandbox MCP. */
|
|
4
5
|
export const SANDBOX_RUN_TOOL_NAME = 'sandbox_run';
|
|
@@ -70,10 +71,10 @@ export function createDefaultAuthBuilder(): SandboxAuthBuilder {
|
|
|
70
71
|
if (sandboxDid) {
|
|
71
72
|
try {
|
|
72
73
|
const invocation = await runCtx.ucan.mintInvocation(
|
|
73
|
-
{ did: sandboxDid, capability:
|
|
74
|
+
{ did: sandboxDid, capability: SANDBOX_CAPABILITY.resource },
|
|
74
75
|
// Claim the ability the user's delegation actually grants. A `'*'`
|
|
75
76
|
// claim is satisfiable only by a `'*'` grant.
|
|
76
|
-
{ can:
|
|
77
|
+
{ can: SANDBOX_CAPABILITY.action },
|
|
77
78
|
);
|
|
78
79
|
if (invocation) {
|
|
79
80
|
headers.Authorization = `Bearer ${invocation}`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { tool as pluginTool } from '../../plugin-api/tool-helper';
|
|
3
3
|
import type { PluginTool, RuntimeContext } from '../../plugin-api/types';
|
|
4
|
+
import { readSandboxResult } from './sandbox-bridge';
|
|
4
5
|
import type { SandboxMcpTool } from './sandbox.plugin';
|
|
5
6
|
|
|
6
7
|
const sandboxWriteBlobSchema = z.object({
|
|
@@ -80,10 +81,19 @@ export function createSandboxWriteBlobTool(
|
|
|
80
81
|
content: blob.value,
|
|
81
82
|
encoding: 'utf8',
|
|
82
83
|
});
|
|
84
|
+
// The sandbox reports a rejected write (bad path, quota) as a
|
|
85
|
+
// returned `success:false` envelope, not a throw.
|
|
86
|
+
const outcome = readSandboxResult(result);
|
|
87
|
+
if (!outcome.ok) {
|
|
88
|
+
return JSON.stringify({
|
|
89
|
+
success: false,
|
|
90
|
+
error: `sandbox_write_file failed: ${outcome.text}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
83
93
|
return JSON.stringify({
|
|
84
94
|
success: true,
|
|
85
95
|
path,
|
|
86
|
-
bytesWritten: blob.value.
|
|
96
|
+
bytesWritten: new TextEncoder().encode(blob.value).byteLength,
|
|
87
97
|
blobName: blob.name,
|
|
88
98
|
note: typeof result === 'string' ? result : undefined,
|
|
89
99
|
});
|
|
@@ -7,13 +7,17 @@ import type {
|
|
|
7
7
|
PluginTool,
|
|
8
8
|
RuntimeContext,
|
|
9
9
|
} from '../../plugin-api/types';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
defaultSandboxMcpClientFactory,
|
|
12
|
+
SANDBOX_MCP_TIMEOUT_MS,
|
|
13
|
+
} from './sandbox-bridge';
|
|
11
14
|
import {
|
|
12
15
|
createDefaultAuthBuilder,
|
|
13
16
|
parseOracleSecrets,
|
|
14
17
|
type SandboxAuthBuilder,
|
|
15
18
|
} from './sandbox-mcp';
|
|
16
19
|
import { createSandboxWriteBlobTool } from './sandbox-write-blob';
|
|
20
|
+
import { SANDBOX_CAPABILITY } from '../delegated-capabilities';
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* Env this plugin owns. Unlike the Node runtime — whose Tier-0 base schema
|
|
@@ -131,6 +135,7 @@ const manifest: PluginManifest = {
|
|
|
131
135
|
stability: 'stable',
|
|
132
136
|
category: 'core',
|
|
133
137
|
tags: ['sandbox', 'execution', 'workspace', 'artifacts'],
|
|
138
|
+
requires: [SANDBOX_CAPABILITY],
|
|
134
139
|
};
|
|
135
140
|
|
|
136
141
|
/** Minimal MCP-client surface — declared structurally so tests can stub it. */
|
|
@@ -151,9 +156,6 @@ export type SandboxMcpClientFactory = (
|
|
|
151
156
|
config: ClientConfig,
|
|
152
157
|
) => SandboxMcpClientLike;
|
|
153
158
|
|
|
154
|
-
/** Per-tool timeout for sandbox MCP calls (matches the Node runtime wiring). */
|
|
155
|
-
const SANDBOX_MCP_TIMEOUT_MS = 180_000;
|
|
156
|
-
|
|
157
159
|
/**
|
|
158
160
|
* How long a request's lazily-connected MCP client survives after its last
|
|
159
161
|
* invocation before being closed (a later call reconnects).
|
|
@@ -349,12 +351,7 @@ export class SandboxPlugin extends OraclePlugin {
|
|
|
349
351
|
return this.toPluginTools(lazyUpstream, rtCtx);
|
|
350
352
|
}
|
|
351
353
|
|
|
352
|
-
const
|
|
353
|
-
const userSecretKeys = Object.keys(userSecretIndex);
|
|
354
|
-
const userSecrets: Record<string, string> =
|
|
355
|
-
userSecretKeys.length > 0
|
|
356
|
-
? await rtCtx.secrets.getValues(userSecretKeys)
|
|
357
|
-
: {};
|
|
354
|
+
const userSecrets = await rtCtx.secrets.getAll();
|
|
358
355
|
|
|
359
356
|
const headers = await this.authBuilder(
|
|
360
357
|
{
|
|
@@ -524,12 +521,7 @@ export class SandboxPlugin extends OraclePlugin {
|
|
|
524
521
|
rtCtx: RuntimeContext;
|
|
525
522
|
}): Promise<{ client: SandboxMcpClientLike; tools: SandboxMcpTool[] }> {
|
|
526
523
|
const { sandboxMcpUrl, skillsServiceUrl, oracleSecrets, rtCtx } = args;
|
|
527
|
-
const
|
|
528
|
-
const userSecretKeys = Object.keys(userSecretIndex);
|
|
529
|
-
const userSecrets: Record<string, string> =
|
|
530
|
-
userSecretKeys.length > 0
|
|
531
|
-
? await rtCtx.secrets.getValues(userSecretKeys)
|
|
532
|
-
: {};
|
|
524
|
+
const userSecrets = await rtCtx.secrets.getAll();
|
|
533
525
|
|
|
534
526
|
const headers = await this.authBuilder(
|
|
535
527
|
{ sandboxMcpUrl, skillsServiceUrl, oracleSecrets, userSecrets },
|