claude-flow 3.62.0 → 3.63.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 +1 -1
- package/package.json +1 -1
- package/v3/@claude-flow/cli/README.md +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +3 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.d.ts +1 -20
- package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.js +344 -108
- package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-learning-tool.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-learning-tool.js +46 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +6 -3
- package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +12 -12
- package/v3/@claude-flow/cli/dist/src/services/managed-learning-loop.d.ts +79 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-learning-loop.js +76 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-learning-store.d.ts +187 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-learning-store.js +238 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-learning.d.ts +108 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-learning.js +197 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-memory-client.d.ts +72 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-memory-client.js +236 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-memory-worker.d.ts +56 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-memory-worker.js +161 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-run-collector.d.ts +93 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-run-collector.js +181 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-run-store.d.ts +52 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-run-store.js +119 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-workflows.d.ts +101 -0
- package/v3/@claude-flow/cli/dist/src/services/managed-workflows.js +183 -0
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -17,7 +17,19 @@
|
|
|
17
17
|
* managed_agent_list → GET /v1/sessions
|
|
18
18
|
* managed_agent_terminate→ DELETE /v1/sessions/{id} (+ optionally the env) ↔ wasm_agent_terminate
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
import { managedLearningTool } from './managed-learning-tool.js';
|
|
21
|
+
import { ManagedRunCollector } from '../services/managed-run-collector.js';
|
|
22
|
+
import { collectWithLease, executeManagedControl, publishManagedSnapshot, publishManagedTermination } from '../services/managed-run-store.js';
|
|
23
|
+
import { getProjectCwd } from './types.js';
|
|
24
|
+
import { dynamicConfig, eventPage, readManagedEvents, ManagedProgress } from '../services/managed-workflows.js';
|
|
25
|
+
// Dedicated override avoids forwarding credentials to a generic model proxy.
|
|
26
|
+
function apiBase() {
|
|
27
|
+
const url = new URL(process.env.RUFLO_MANAGED_AGENTS_BASE_URL || 'https://api.anthropic.com');
|
|
28
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') {
|
|
29
|
+
throw new Error('Managed Agents endpoint must be an HTTPS origin');
|
|
30
|
+
}
|
|
31
|
+
return url.origin;
|
|
32
|
+
}
|
|
21
33
|
const BETA_HEADER = 'managed-agents-2026-04-01';
|
|
22
34
|
const ANTHROPIC_VERSION = '2023-06-01';
|
|
23
35
|
const DEFAULT_MODEL = 'claude-sonnet-4-6';
|
|
@@ -38,84 +50,165 @@ const NEEDS_KEY = {
|
|
|
38
50
|
error: 'managed-agent runtime needs ANTHROPIC_API_KEY (or CLAUDE_API_KEY) and Claude Managed Agents beta access. ' +
|
|
39
51
|
'For a local, no-key agent runtime use wasm_agent_create instead (rvagent / WASM sandbox).',
|
|
40
52
|
};
|
|
41
|
-
async function maRequest(method, path, body) {
|
|
53
|
+
async function maRequest(method, path, body, timeoutMs = 30_000) {
|
|
42
54
|
const key = apiKey();
|
|
43
55
|
if (!key)
|
|
44
56
|
return { ok: false, status: 0, error: NEEDS_KEY.error };
|
|
45
|
-
let res;
|
|
46
57
|
try {
|
|
47
|
-
res = await fetch(`${
|
|
48
|
-
method,
|
|
49
|
-
headers: headers(key),
|
|
58
|
+
const res = await fetch(`${apiBase()}/v1${path}`, {
|
|
59
|
+
method, headers: headers(key), redirect: 'error',
|
|
50
60
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
51
|
-
signal: AbortSignal.timeout(30_000),
|
|
61
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(timeoutMs, 30_000))),
|
|
52
62
|
});
|
|
63
|
+
// Never echo upstream bodies: they may contain prompts, keys or MCP tokens.
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
await res.body?.cancel();
|
|
66
|
+
return { ok: false, status: res.status, error: `Managed Agents API HTTP ${res.status}` };
|
|
67
|
+
}
|
|
68
|
+
if (method === 'DELETE' && ![200, 204].includes(res.status)) {
|
|
69
|
+
await res.body?.cancel();
|
|
70
|
+
return { ok: false, status: res.status, error: 'Provider deletion is not yet confirmed' };
|
|
71
|
+
}
|
|
72
|
+
if (res.status === 204)
|
|
73
|
+
return { ok: true, status: res.status, data: undefined };
|
|
74
|
+
const reader = res.body?.getReader();
|
|
75
|
+
const chunks = [];
|
|
76
|
+
let bytes = 0;
|
|
77
|
+
if (reader) {
|
|
78
|
+
try {
|
|
79
|
+
while (true) {
|
|
80
|
+
const chunk = await reader.read();
|
|
81
|
+
if (chunk.done)
|
|
82
|
+
break;
|
|
83
|
+
bytes += chunk.value.byteLength;
|
|
84
|
+
if (bytes > 8_000_000) {
|
|
85
|
+
await reader.cancel();
|
|
86
|
+
throw new Error('response limit');
|
|
87
|
+
}
|
|
88
|
+
chunks.push(chunk.value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
reader.releaseLock();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
96
|
+
return { ok: true, status: res.status, data: (text ? JSON.parse(text) : undefined) };
|
|
53
97
|
}
|
|
54
|
-
catch
|
|
55
|
-
return { ok: false, status: 0, error:
|
|
56
|
-
}
|
|
57
|
-
const text = await res.text();
|
|
58
|
-
let parsed = undefined;
|
|
59
|
-
try {
|
|
60
|
-
parsed = text ? JSON.parse(text) : undefined;
|
|
61
|
-
}
|
|
62
|
-
catch { /* leave undefined */ }
|
|
63
|
-
if (!res.ok) {
|
|
64
|
-
const apiErr = parsed?.error?.message
|
|
65
|
-
?? parsed?.message
|
|
66
|
-
?? text.slice(0, 300);
|
|
67
|
-
return { ok: false, status: res.status, error: `Managed Agents API ${res.status}: ${apiErr}`, body: parsed };
|
|
98
|
+
catch {
|
|
99
|
+
return { ok: false, status: 0, error: 'Managed Agents transport or response error; inspect provider status before retrying mutations' };
|
|
68
100
|
}
|
|
69
|
-
return { ok: true, status: res.status, data: parsed };
|
|
70
101
|
}
|
|
71
|
-
// ---- helpers --------------------------------------------------------------
|
|
72
102
|
function summarizeEvents(events) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
else if (e.type === 'session.status_running') {
|
|
85
|
-
status = 'running';
|
|
86
|
-
}
|
|
87
|
-
else if (e.type === 'session.status_idle') {
|
|
88
|
-
status = 'idle';
|
|
89
|
-
stopReason = e.stop_reason?.type ?? null;
|
|
90
|
-
}
|
|
91
|
-
else if (e.type === 'session.status_error' || e.type === 'session.status_failed') {
|
|
92
|
-
status = 'error';
|
|
103
|
+
const progress = new ManagedProgress();
|
|
104
|
+
progress.accept(events);
|
|
105
|
+
return progress.snapshot();
|
|
106
|
+
}
|
|
107
|
+
async function fetchSessionEvents(sessionId, since, threadId, deadline = Date.now() + 30_000, expectedScope = currentCollectorScope()) {
|
|
108
|
+
return readManagedEvents(async (query) => {
|
|
109
|
+
const thread = threadId ? `/threads/${encodeURIComponent(threadId)}` : '';
|
|
110
|
+
const params = new URLSearchParams(query);
|
|
111
|
+
if (threadId) {
|
|
112
|
+
params.delete('order');
|
|
113
|
+
params.delete('created_at[gte]');
|
|
93
114
|
}
|
|
115
|
+
if (currentCollectorScope() !== expectedScope)
|
|
116
|
+
throw new Error('Managed credential scope changed during observation');
|
|
117
|
+
if (Date.now() >= deadline)
|
|
118
|
+
throw new Error('Managed Agents observation deadline reached');
|
|
119
|
+
const r = await maRequest('GET', `/sessions/${encodeURIComponent(sessionId)}${thread}/events?${params}`, undefined, deadline - Date.now());
|
|
120
|
+
if (!r.ok)
|
|
121
|
+
throw new Error(r.error);
|
|
122
|
+
if (currentCollectorScope() !== expectedScope)
|
|
123
|
+
throw new Error('Managed credential scope changed during observation');
|
|
124
|
+
return r.data;
|
|
125
|
+
}, since);
|
|
126
|
+
}
|
|
127
|
+
let collectorScope = '';
|
|
128
|
+
let collector = new ManagedRunCollector();
|
|
129
|
+
// The scope only answers "did the project, endpoint or credential change since this collector was built". It never leaves the
|
|
130
|
+
// process, so it is a generation number, not a digest: the API key is compared in memory (it already lives in the environment)
|
|
131
|
+
// and is never fed to a hash function. Returning to an earlier value counts as a change, which is the safe direction.
|
|
132
|
+
let scopeInput = '';
|
|
133
|
+
let scopeGeneration = 0;
|
|
134
|
+
function currentCollectorScope() {
|
|
135
|
+
const input = JSON.stringify([getProjectCwd(), apiBase(), apiKey()]);
|
|
136
|
+
if (input !== scopeInput) {
|
|
137
|
+
scopeInput = input;
|
|
138
|
+
scopeGeneration++;
|
|
94
139
|
}
|
|
95
|
-
return {
|
|
140
|
+
return `scope-${scopeGeneration}`;
|
|
96
141
|
}
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
142
|
+
function scopedCollector() {
|
|
143
|
+
const scope = currentCollectorScope();
|
|
144
|
+
if (scope !== collectorScope) {
|
|
145
|
+
collector = new ManagedRunCollector();
|
|
146
|
+
collectorScope = scope;
|
|
147
|
+
}
|
|
148
|
+
return collector;
|
|
103
149
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
150
|
+
async function observeSession(sessionId, deadline = Date.now() + 30_000) {
|
|
151
|
+
const root = getProjectCwd(), active = scopedCollector(), expectedScope = collectorScope;
|
|
152
|
+
return collectWithLease(root, sessionId, async () => {
|
|
153
|
+
const result = await active.refresh(sessionId, {
|
|
154
|
+
readEvents: since => fetchSessionEvents(sessionId, since, undefined, deadline, expectedScope),
|
|
155
|
+
readSession: async () => {
|
|
156
|
+
if (currentCollectorScope() !== expectedScope)
|
|
157
|
+
throw new Error('Managed credential scope changed during observation');
|
|
158
|
+
const r = await maRequest('GET', `/sessions/${encodeURIComponent(sessionId)}`, undefined, deadline - Date.now());
|
|
159
|
+
if (!r.ok)
|
|
160
|
+
throw new Error(r.error);
|
|
161
|
+
return r.data;
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
if (currentCollectorScope() !== expectedScope)
|
|
165
|
+
throw new Error('Managed credential scope changed during observation');
|
|
166
|
+
// The snapshot only feeds the console. A write failure (size cap, stale lock) must not fail a successful provider read.
|
|
167
|
+
try {
|
|
168
|
+
publishManagedSnapshot(root, result.snapshot);
|
|
169
|
+
}
|
|
170
|
+
catch { /* best effort */ }
|
|
171
|
+
return result;
|
|
172
|
+
}, expectedScope);
|
|
173
|
+
}
|
|
174
|
+
// Gate new-turn observations on the provider's acknowledged message ID, not
|
|
175
|
+
// on a local timestamp or a historical idle event. Calls are serialized locally.
|
|
176
|
+
const pendingPrompts = new Set();
|
|
177
|
+
async function waitForIdle(sessionId, maxWaitMs, messageId) {
|
|
107
178
|
const deadline = Date.now() + Math.max(1_000, Math.min(maxWaitMs, 600_000));
|
|
108
|
-
|
|
179
|
+
const progress = new ManagedProgress();
|
|
180
|
+
let started = false;
|
|
109
181
|
while (Date.now() < deadline) {
|
|
110
|
-
events = await
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
182
|
+
const { events } = await observeSession(sessionId, deadline);
|
|
183
|
+
if (!started) {
|
|
184
|
+
const boundary = events.findIndex(e => e.id === messageId && !!e.processed_at);
|
|
185
|
+
if (boundary >= 0) {
|
|
186
|
+
// Existing runs remain relevant when a user steers an active session.
|
|
187
|
+
const prior = new ManagedProgress();
|
|
188
|
+
const history = events.slice(0, boundary).filter(e => e.type.startsWith('workflow_run.'));
|
|
189
|
+
prior.accept(history);
|
|
190
|
+
const open = new Set(prior.snapshot().workflowRuns.filter(r => r.status !== 'ended').map(r => r.id));
|
|
191
|
+
progress.accept(history.filter(e => e.workflow_run_id && open.has(e.workflow_run_id)));
|
|
192
|
+
progress.accept(events.slice(boundary));
|
|
193
|
+
started = true;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// The provider returns the whole session history on every read. Keep ignoring everything before the
|
|
198
|
+
// acknowledged message: its old end_turn idle would otherwise be applied on a later poll and report
|
|
199
|
+
// the previous turn's answer as finished while the new turn is still running.
|
|
200
|
+
const boundary = events.findIndex(e => e.id === messageId);
|
|
201
|
+
if (boundary >= 0)
|
|
202
|
+
progress.accept(events.slice(boundary));
|
|
203
|
+
}
|
|
204
|
+
const state = progress.snapshot();
|
|
205
|
+
if (started && (state.finished || state.blocked || (state.interrupted && state.status === 'idle') || state.status === 'terminated'))
|
|
206
|
+
return state;
|
|
207
|
+
await new Promise(r => setTimeout(r, Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()))));
|
|
116
208
|
}
|
|
117
|
-
return
|
|
209
|
+
return progress.snapshot();
|
|
118
210
|
}
|
|
211
|
+
function definiteRejection(status) { return [400, 401, 403, 404, 422].includes(status); }
|
|
119
212
|
function maName(prefix, given) {
|
|
120
213
|
if (typeof given === 'string' && given.trim())
|
|
121
214
|
return given.trim().slice(0, 80);
|
|
@@ -123,9 +216,62 @@ function maName(prefix, given) {
|
|
|
123
216
|
}
|
|
124
217
|
// ---------------------------------------------------------------------------
|
|
125
218
|
export const managedAgentTools = [
|
|
219
|
+
{
|
|
220
|
+
name: 'managed_agent_capabilities',
|
|
221
|
+
description: 'Inspect local Managed Agents adapter capabilities without network access or an API key. Use when native tool registration alone cannot establish adapter requirements, budget semantics or verification boundaries. Reports configuration facts only; it never verifies beta access, remote health or authorization. Pair with /agent-mod capabilities for plugin policy and registered tools; use wasm_agent_* for local execution.',
|
|
222
|
+
category: 'agent',
|
|
223
|
+
inputSchema: { type: 'object', properties: {} },
|
|
224
|
+
handler: async () => ({
|
|
225
|
+
runtime: 'managed', provider: 'anthropic', networkCalled: false,
|
|
226
|
+
credentialConfigured: !!apiKey(), betaAccess: 'unknown', remoteHealth: 'unknown', authorization: 'host-policy-required',
|
|
227
|
+
features: ['dynamic-workflows', 'budget-at-creation', 'limited-networking', 'predefined-agents', 'thread-inspection', 'interrupt-request', 'session-delete', 'verified-learning'],
|
|
228
|
+
create: { dynamicWorkflows: true, budgetCents: 'positive integer US cents string', networking: 'limited', predefinedAgents: 'optional agent IDs; required by default mod policy' },
|
|
229
|
+
controls: { budgetOvershoot: 'in-flight requests may finish', promptTimeout: 'observation only', interrupt: 'pause requested, not cancellation', verification: 'unverified' },
|
|
230
|
+
integrations: { agentMod: '/agent-mod capabilities', workflowMod: '/wf-mod managed', console: 'Mods page reads agent-mod/status.json', evidence: 'independently verify before memory_* retention or RVF export' },
|
|
231
|
+
learning: { enabled: process.env.RUFLO_MANAGED_LEARNING === '1', verifierConfigured: !!process.env.RUFLO_MANAGED_VERIFIER_PUBLIC_KEY, defaultsChanged: false },
|
|
232
|
+
scope: 'Adapter facts do not grant tools, credentials, plugins or release authority. Mod admission is host-session-local, not global spend enforcement.',
|
|
233
|
+
}),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'managed_agent_learning',
|
|
237
|
+
description: 'Record structured workflow outcomes, inspect learning, recommend strategies, evaluate paired holdouts, or execute one bounded feedback cycle. Use when provider completion cannot establish correctness or teach routing safely. Verified evidence needs the host Ed25519 verifier. Native memory runs in isolated workers. MetaHarness eligibility is advisory and never changes defaults. Pair with managed_agent_events and managed_agent_control for provider observations and policy checked controls; use wasm_agent_* for local execution.',
|
|
238
|
+
category: 'agent', inputSchema: managedLearningTool.inputSchema, handler: managedLearningTool.handler,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'managed_agent_control',
|
|
242
|
+
description: 'Inspect, request interruption, or terminate one managed session through fresh host policy and provider-state checks. Use when native console snapshots are too stale to authorize a remote action. Requires host RUFLO_MANAGED_CONTROL=1 and an exact RUFLO_MANAGED_CONTROL_SESSIONS allowlist. No caller approval flag is accepted. Pair with managed_agent_events; use wasm_agent_terminate for local agents.',
|
|
243
|
+
category: 'agent',
|
|
244
|
+
inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, action: { type: 'string', enum: ['inspect', 'interrupt', 'terminate'] }, threadOffset: { type: 'integer', minimum: 0, maximum: 9999 } }, required: ['sessionId', 'action'] },
|
|
245
|
+
handler: async (input) => {
|
|
246
|
+
if (!apiKey())
|
|
247
|
+
return { success: false, ...NEEDS_KEY };
|
|
248
|
+
try {
|
|
249
|
+
const request = async (method, id, body) => {
|
|
250
|
+
const r = await maRequest(method, `/sessions/${encodeURIComponent(id)}${method === 'POST' ? '/events' : ''}`, body);
|
|
251
|
+
if (!r.ok)
|
|
252
|
+
throw new Error(r.error);
|
|
253
|
+
return r.data;
|
|
254
|
+
};
|
|
255
|
+
return await executeManagedControl(input, {
|
|
256
|
+
policy: () => ({ enabled: process.env.RUFLO_MANAGED_CONTROL === '1', sessions: (process.env.RUFLO_MANAGED_CONTROL_SESSIONS ?? '').split(',').map(s => s.trim()).filter(Boolean) }),
|
|
257
|
+
read: id => request('GET', id),
|
|
258
|
+
inspect: async (id, offset) => { await observeSession(id); const page = scopedCollector().page(id, offset); if (!page)
|
|
259
|
+
throw new Error('Snapshot unavailable'); publishManagedSnapshot(getProjectCwd(), page); return page; },
|
|
260
|
+
interrupt: id => request('POST', id, { events: [{ type: 'user.interrupt' }] }),
|
|
261
|
+
terminate: async (id) => { const result = await request('DELETE', id); try {
|
|
262
|
+
publishManagedTermination(getProjectCwd(), id, scopedCollector());
|
|
263
|
+
}
|
|
264
|
+
catch { /* the provider deletion is the fact; the local snapshot is best-effort */ } return result; },
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return { success: false, error: 'Managed action refused: recheck current host policy, provider state and observation lease' };
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
},
|
|
126
272
|
{
|
|
127
273
|
name: 'managed_agent_create',
|
|
128
|
-
description: 'Spin up an Anthropic-managed cloud agent (Agent + Environment + Session) — the CLOUD counterpart of wasm_agent_create. Use when wasm_agent_create (local WASM sandbox) is wrong because the task is long-running/async (minutes-hours), needs a real cloud container with pre-installed packages + network, or persistent filesystem + transcript across turns. For a fast, free, ephemeral, offline agent use wasm_agent_create (rvagent). Needs ANTHROPIC_API_KEY + Managed Agents beta access. Returns {sessionId, agentId, environmentId}; pair with managed_agent_prompt.',
|
|
274
|
+
description: 'Spin up an Anthropic-managed cloud agent (Agent + Environment + Session) — the CLOUD counterpart of wasm_agent_create. Use when wasm_agent_create (local WASM sandbox) is wrong because the task is long-running/async (minutes-hours), needs a real cloud container with pre-installed packages + network, or persistent filesystem + transcript across turns. For a fast, free, ephemeral, offline agent use wasm_agent_create (rvagent). Needs ANTHROPIC_API_KEY + Managed Agents beta access. Opt into dynamic workflows with dynamicWorkflows=true and mandatory budgetCents (integer US cents); networking is then limited. Returns {sessionId, agentId, environmentId}; pair with managed_agent_prompt.',
|
|
129
275
|
category: 'agent',
|
|
130
276
|
inputSchema: {
|
|
131
277
|
type: 'object',
|
|
@@ -141,13 +287,24 @@ export const managedAgentTools = [
|
|
|
141
287
|
},
|
|
142
288
|
skills: { type: 'array', description: 'Skills to attach to the agent', items: { type: 'object' } },
|
|
143
289
|
packages: { type: 'object', description: 'Environment packages: {pip?:[], npm?:[], apt?:[], cargo?:[], gem?:[], go?:[]}' },
|
|
144
|
-
networking: { type: 'string', enum: ['unrestricted', 'restricted', 'none'], description: '
|
|
290
|
+
networking: { type: 'string', enum: ['unrestricted', 'restricted', 'none', 'limited'], description: 'Legacy default unrestricted; dynamic workflows require limited' },
|
|
291
|
+
dynamicWorkflows: { type: 'boolean', description: 'Opt in to multiagent_20261001 dynamic workflows (requires budgetCents)' },
|
|
292
|
+
budgetCents: { type: 'string', pattern: '^[1-9][0-9]{0,8}$', description: 'Positive integer US cents string; 2500 means $25; in-flight requests may overshoot' },
|
|
293
|
+
allowedHosts: { type: 'array', items: { type: 'string' }, description: 'Exact allowed hostnames for dynamic workflow limited networking' },
|
|
294
|
+
predefinedAgents: { type: 'array', minItems: 1, maxItems: 20, items: { type: 'string' }, description: 'Restrict workflows to these agent IDs; disables inline agents' },
|
|
145
295
|
initScript: { type: 'string', description: 'Environment init script (bash, run at container start)' },
|
|
146
296
|
},
|
|
147
297
|
},
|
|
148
298
|
handler: async (input) => {
|
|
149
299
|
if (!apiKey())
|
|
150
300
|
return { success: false, ...NEEDS_KEY };
|
|
301
|
+
let config;
|
|
302
|
+
try {
|
|
303
|
+
config = dynamicConfig(input);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
return { success: false, error: error.message };
|
|
307
|
+
}
|
|
151
308
|
// 1. Agent
|
|
152
309
|
const agentBody = {
|
|
153
310
|
name: maName('ruflo-managed', input.name),
|
|
@@ -156,34 +313,51 @@ export const managedAgentTools = [
|
|
|
156
313
|
};
|
|
157
314
|
if (typeof input.system === 'string')
|
|
158
315
|
agentBody.system = input.system;
|
|
316
|
+
if (config.multiagent)
|
|
317
|
+
agentBody.multiagent = config.multiagent;
|
|
159
318
|
if (Array.isArray(input.mcpServers) && input.mcpServers.length)
|
|
160
319
|
agentBody.mcp_servers = input.mcpServers;
|
|
161
320
|
if (Array.isArray(input.skills) && input.skills.length)
|
|
162
321
|
agentBody.skills = input.skills;
|
|
163
322
|
const a = await maRequest('POST', '/agents', agentBody);
|
|
164
323
|
if (!a.ok)
|
|
165
|
-
return { success: false, stage: 'agent', error: a.error };
|
|
324
|
+
return { success: false, stage: 'agent', error: a.error, cleanupRequired: !definiteRejection(a.status) };
|
|
325
|
+
if (!a.data || typeof a.data.id !== 'string' || !/^agent_[A-Za-z0-9]+$/.test(a.data.id))
|
|
326
|
+
return { success: false, stage: 'agent', error: 'Invalid agent acknowledgement; inspect provider resources before retrying', cleanupRequired: true };
|
|
166
327
|
// 2. Environment
|
|
167
|
-
const net = input.networking || 'unrestricted';
|
|
168
328
|
const envBody = {
|
|
169
329
|
name: maName('ruflo-managed-env', input.name),
|
|
170
|
-
config: { type: 'cloud', networking:
|
|
330
|
+
config: { type: 'cloud', networking: config.networking },
|
|
171
331
|
};
|
|
172
332
|
if (input.packages && typeof input.packages === 'object')
|
|
173
333
|
envBody.config.packages = { type: 'packages', ...input.packages };
|
|
174
334
|
if (typeof input.initScript === 'string' && input.initScript)
|
|
175
335
|
envBody.config.init_script = input.initScript;
|
|
176
336
|
const e = await maRequest('POST', '/environments', envBody);
|
|
177
|
-
if (!e.ok)
|
|
178
|
-
|
|
337
|
+
if (!e.ok) {
|
|
338
|
+
if (!definiteRejection(e.status))
|
|
339
|
+
return { success: false, stage: 'environment', agentId: a.data.id, error: e.error, cleanupRequired: true };
|
|
340
|
+
const cleanup = await maRequest('DELETE', `/agents/${encodeURIComponent(a.data.id)}`);
|
|
341
|
+
return { success: false, stage: 'environment', agentId: a.data.id, error: e.error, agentDeleted: cleanup.ok };
|
|
342
|
+
}
|
|
343
|
+
if (!e.data || typeof e.data.id !== 'string' || !/^env_[A-Za-z0-9]+$/.test(e.data.id))
|
|
344
|
+
return { success: false, stage: 'environment', agentId: a.data.id, error: 'Invalid environment acknowledgement; inspect provider resources before retrying', cleanupRequired: true };
|
|
179
345
|
// 3. Session
|
|
180
346
|
const s = await maRequest('POST', '/sessions', {
|
|
181
347
|
agent: a.data.id,
|
|
182
348
|
environment_id: e.data.id,
|
|
183
349
|
title: maName('ruflo-managed session', input.title),
|
|
350
|
+
...(config.budget ? { budget: config.budget } : {}),
|
|
184
351
|
});
|
|
185
|
-
if (!s.ok)
|
|
186
|
-
|
|
352
|
+
if (!s.ok) {
|
|
353
|
+
if (!definiteRejection(s.status))
|
|
354
|
+
return { success: false, stage: 'session', agentId: a.data.id, environmentId: e.data.id, error: s.error, cleanupRequired: true };
|
|
355
|
+
const envCleanup = await maRequest('DELETE', `/environments/${encodeURIComponent(e.data.id)}`);
|
|
356
|
+
const agentCleanup = await maRequest('DELETE', `/agents/${encodeURIComponent(a.data.id)}`);
|
|
357
|
+
return { success: false, stage: 'session', agentId: a.data.id, environmentId: e.data.id, error: s.error, environmentDeleted: envCleanup.ok, agentDeleted: agentCleanup.ok };
|
|
358
|
+
}
|
|
359
|
+
if (!s.data || typeof s.data.id !== 'string' || !/^sesn_[A-Za-z0-9]+$/.test(s.data.id))
|
|
360
|
+
return { success: false, stage: 'session', agentId: a.data.id, environmentId: e.data.id, error: 'Invalid session acknowledgement; inspect provider resources before retrying', cleanupRequired: true };
|
|
187
361
|
return {
|
|
188
362
|
success: true,
|
|
189
363
|
runtime: 'managed',
|
|
@@ -193,7 +367,10 @@ export const managedAgentTools = [
|
|
|
193
367
|
environmentId: e.data.id,
|
|
194
368
|
status: s.data.status ?? 'idle',
|
|
195
369
|
model: agentBody.model,
|
|
196
|
-
|
|
370
|
+
dynamicWorkflows: config.enabled,
|
|
371
|
+
budget: config.budget,
|
|
372
|
+
verification: 'unverified',
|
|
373
|
+
note: 'Cloud agent provisioned. Send work with managed_agent_prompt({sessionId, message}); inspect with managed_agent_events; clean up with managed_agent_terminate (running sessions consume runtime and tokens).',
|
|
197
374
|
};
|
|
198
375
|
},
|
|
199
376
|
},
|
|
@@ -219,28 +396,38 @@ export const managedAgentTools = [
|
|
|
219
396
|
return { success: false, error: 'sessionId is required' };
|
|
220
397
|
if (!message)
|
|
221
398
|
return { success: false, error: 'message is required' };
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
:
|
|
243
|
-
|
|
399
|
+
if (input.maxWaitMs !== undefined && (typeof input.maxWaitMs !== 'number' || !Number.isFinite(input.maxWaitMs) || input.maxWaitMs <= 0))
|
|
400
|
+
return { success: false, error: 'maxWaitMs must be finite and positive' };
|
|
401
|
+
if (pendingPrompts.has(sessionId))
|
|
402
|
+
return { success: false, error: 'A prompt is already pending for this session' };
|
|
403
|
+
pendingPrompts.add(sessionId);
|
|
404
|
+
let sentMessageId;
|
|
405
|
+
try {
|
|
406
|
+
const send = await maRequest('POST', `/sessions/${encodeURIComponent(sessionId)}/events`, {
|
|
407
|
+
events: [{ type: 'user.message', content: [{ type: 'text', text: message }] }],
|
|
408
|
+
});
|
|
409
|
+
if (!send.ok)
|
|
410
|
+
return { success: false, stage: 'send', sessionId, error: send.error };
|
|
411
|
+
const sent = eventPage(send.data).data.find(e => e.type === 'user.message');
|
|
412
|
+
if (!sent)
|
|
413
|
+
return { success: false, sessionId, error: 'Missing message acknowledgement; inspect events before retrying' };
|
|
414
|
+
sentMessageId = sent.id;
|
|
415
|
+
const maxWait = typeof input.maxWaitMs === 'number' ? input.maxWaitMs : DEFAULT_MAX_WAIT_MS;
|
|
416
|
+
const state = await waitForIdle(sessionId, maxWait, sent.id);
|
|
417
|
+
return {
|
|
418
|
+
success: state.observedFailures === 0 && state.status !== 'terminated',
|
|
419
|
+
runtime: 'managed', sessionId, messageId: sent.id, ...state,
|
|
420
|
+
note: 'Provider output is unverified. Inspect workflow threads and independently test artifacts. A wait timeout does not stop remote execution; inspect events or terminate explicitly.',
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
// Once the provider acknowledged the message, say so: a retry would send it twice.
|
|
425
|
+
return { success: false, sessionId, finished: false, error: error.message,
|
|
426
|
+
...(sentMessageId ? { messageId: sentMessageId, note: 'The message was already sent; inspect events instead of resending.' } : {}) };
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
pendingPrompts.delete(sessionId);
|
|
430
|
+
}
|
|
244
431
|
},
|
|
245
432
|
},
|
|
246
433
|
{
|
|
@@ -268,6 +455,7 @@ export const managedAgentTools = [
|
|
|
268
455
|
type: 'object',
|
|
269
456
|
properties: {
|
|
270
457
|
sessionId: { type: 'string', description: 'Session id' },
|
|
458
|
+
threadId: { type: 'string', description: 'Optional child thread ID from session.thread_created; inspect worker failures' },
|
|
271
459
|
raw: { type: 'boolean', description: 'Include the full raw event objects (default: summary + compact list)' },
|
|
272
460
|
},
|
|
273
461
|
required: ['sessionId'],
|
|
@@ -278,25 +466,27 @@ export const managedAgentTools = [
|
|
|
278
466
|
const sessionId = String(input.sessionId ?? '');
|
|
279
467
|
if (!sessionId)
|
|
280
468
|
return { error: 'sessionId is required' };
|
|
281
|
-
|
|
469
|
+
let events;
|
|
470
|
+
try {
|
|
471
|
+
events = typeof input.threadId === 'string' ? await fetchSessionEvents(sessionId, undefined, input.threadId) : (await observeSession(sessionId)).events;
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
return { success: false, sessionId, error: error.message };
|
|
475
|
+
}
|
|
282
476
|
const sum = summarizeEvents(events);
|
|
283
477
|
const compact = events.map(e => {
|
|
284
478
|
if (e.type === 'agent.message')
|
|
285
|
-
return { type: e.type, text: (e.content ?? []).filter(b => b
|
|
479
|
+
return { type: e.type, text: (e.content ?? []).filter(b => b?.type === 'text').map(b => b.text ?? '').join('').slice(0, 500) };
|
|
286
480
|
if (e.type === 'agent.tool_use')
|
|
287
481
|
return { type: e.type, name: e.name, input: e.input };
|
|
288
482
|
if (e.type === 'agent.tool_result')
|
|
289
483
|
return { type: e.type, content: JSON.stringify(e.content ?? {}).slice(0, 500) };
|
|
290
|
-
return { type: e.type, ...(e.stop_reason ? { stop_reason: e.stop_reason } : {}) };
|
|
484
|
+
return { id: e.id, type: e.type, workflowRunId: e.workflow_run_id, threadId: e.session_thread_id, result: e.result, ...(e.stop_reason ? { stop_reason: e.stop_reason } : {}) };
|
|
291
485
|
});
|
|
292
486
|
return {
|
|
293
487
|
runtime: 'managed',
|
|
294
488
|
sessionId,
|
|
295
|
-
|
|
296
|
-
stopReason: sum.stopReason,
|
|
297
|
-
assistantText: sum.assistantText,
|
|
298
|
-
toolUses: sum.toolUses,
|
|
299
|
-
eventCount: sum.eventCount,
|
|
489
|
+
...sum,
|
|
300
490
|
events: input.raw ? events : compact,
|
|
301
491
|
};
|
|
302
492
|
},
|
|
@@ -321,9 +511,26 @@ export const managedAgentTools = [
|
|
|
321
511
|
};
|
|
322
512
|
},
|
|
323
513
|
},
|
|
514
|
+
{
|
|
515
|
+
name: 'managed_agent_interrupt',
|
|
516
|
+
description: 'Request an interrupt for an Anthropic managed session. Use when native interruption cannot reach the remote cloud runtime; for local WASM use wasm_agent_terminate. This queues a pause, does not end workflow runs, and never confirms cancellation. Inspect managed_agent_events or explicitly terminate the session.',
|
|
517
|
+
category: 'agent',
|
|
518
|
+
inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Managed session to interrupt' } }, required: ['sessionId'] },
|
|
519
|
+
handler: async (input) => {
|
|
520
|
+
if (!apiKey())
|
|
521
|
+
return { success: false, ...NEEDS_KEY };
|
|
522
|
+
if (typeof input.sessionId !== 'string' || !input.sessionId)
|
|
523
|
+
return { success: false, error: 'sessionId is required' };
|
|
524
|
+
// No ungated path to a remote action: this uses the same host policy, session allowlist and fresh provider-state
|
|
525
|
+
// check as managed_agent_control, so a prompt-injected caller cannot interrupt arbitrary organisation sessions.
|
|
526
|
+
const control = managedAgentTools.find(t => t.name === 'managed_agent_control');
|
|
527
|
+
const r = await control.handler({ sessionId: input.sessionId, action: 'interrupt' });
|
|
528
|
+
return { ...r, sessionId: input.sessionId, interruptRequested: r.success === true, cancellationConfirmed: false };
|
|
529
|
+
},
|
|
530
|
+
},
|
|
324
531
|
{
|
|
325
532
|
name: 'managed_agent_terminate',
|
|
326
|
-
description: 'Delete a managed cloud-agent session (stops billing for it) — the CLOUD counterpart of wasm_agent_terminate. Use when native nothing applies because a cloud session
|
|
533
|
+
description: 'Delete a managed cloud-agent session (stops billing for it) — the CLOUD counterpart of wasm_agent_terminate. Use when native nothing applies because a running cloud session consumes runtime and tokens. For a local WASM agent use wasm_agent_terminate. Optionally also deletes the session\'s environment. Always call this when done with a managed agent.',
|
|
327
534
|
category: 'agent',
|
|
328
535
|
inputSchema: {
|
|
329
536
|
type: 'object',
|
|
@@ -339,17 +546,46 @@ export const managedAgentTools = [
|
|
|
339
546
|
const sessionId = String(input.sessionId ?? '');
|
|
340
547
|
if (!sessionId)
|
|
341
548
|
return { success: false, error: 'sessionId is required' };
|
|
549
|
+
// Default behaviour is unchanged. Once the host has switched on the governed control plane
|
|
550
|
+
// (RUFLO_MANAGED_CONTROL=1) this tool obeys the same session allowlist, and may delete only that session's own environment.
|
|
551
|
+
const governed = process.env.RUFLO_MANAGED_CONTROL === '1';
|
|
552
|
+
if (governed && !(process.env.RUFLO_MANAGED_CONTROL_SESSIONS ?? '').split(',').map(x => x.trim()).filter(Boolean).includes(sessionId)) {
|
|
553
|
+
return { success: false, error: 'Host managed control policy denies this session' };
|
|
554
|
+
}
|
|
555
|
+
const wantsEnvironment = typeof input.environmentId === 'string' && input.environmentId.length > 0;
|
|
556
|
+
let ownEnvironment;
|
|
557
|
+
if (governed && wantsEnvironment) {
|
|
558
|
+
const g = await maRequest('GET', `/sessions/${encodeURIComponent(sessionId)}`);
|
|
559
|
+
if (!g.ok)
|
|
560
|
+
return { success: false, error: g.error };
|
|
561
|
+
ownEnvironment = g.data.environment_id;
|
|
562
|
+
}
|
|
342
563
|
const s = await maRequest('DELETE', `/sessions/${encodeURIComponent(sessionId)}`);
|
|
343
564
|
const result = { runtime: 'managed', sessionId, sessionDeleted: s.ok };
|
|
565
|
+
// The deletion already happened; a failed local snapshot update must not turn it into an error or skip the environment.
|
|
566
|
+
if (s.ok) {
|
|
567
|
+
try {
|
|
568
|
+
publishManagedTermination(getProjectCwd(), sessionId, scopedCollector());
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
result.snapshotUpdated = false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
344
574
|
if (!s.ok)
|
|
345
575
|
result.error = s.error;
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
576
|
+
if (s.ok && wantsEnvironment) {
|
|
577
|
+
if (governed && input.environmentId !== ownEnvironment) {
|
|
578
|
+
result.environmentDeleted = false;
|
|
579
|
+
result.environmentError = 'environmentId does not belong to this session';
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
const e = await maRequest('DELETE', `/environments/${encodeURIComponent(String(input.environmentId))}`);
|
|
583
|
+
result.environmentDeleted = e.ok;
|
|
584
|
+
if (!e.ok)
|
|
585
|
+
result.environmentError = e.error;
|
|
586
|
+
}
|
|
351
587
|
}
|
|
352
|
-
result.success = s.ok;
|
|
588
|
+
result.success = s.ok && result.environmentDeleted !== false;
|
|
353
589
|
return result;
|
|
354
590
|
},
|
|
355
591
|
},
|