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.
Files changed (27) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/v3/@claude-flow/cli/README.md +1 -1
  4. package/v3/@claude-flow/cli/catalog-manifest.json +3 -3
  5. package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.d.ts +1 -20
  6. package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.js +344 -108
  7. package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-learning-tool.d.ts +4 -0
  8. package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-learning-tool.js +46 -0
  9. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +6 -3
  10. package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +12 -12
  11. package/v3/@claude-flow/cli/dist/src/services/managed-learning-loop.d.ts +79 -0
  12. package/v3/@claude-flow/cli/dist/src/services/managed-learning-loop.js +76 -0
  13. package/v3/@claude-flow/cli/dist/src/services/managed-learning-store.d.ts +187 -0
  14. package/v3/@claude-flow/cli/dist/src/services/managed-learning-store.js +238 -0
  15. package/v3/@claude-flow/cli/dist/src/services/managed-learning.d.ts +108 -0
  16. package/v3/@claude-flow/cli/dist/src/services/managed-learning.js +197 -0
  17. package/v3/@claude-flow/cli/dist/src/services/managed-memory-client.d.ts +72 -0
  18. package/v3/@claude-flow/cli/dist/src/services/managed-memory-client.js +236 -0
  19. package/v3/@claude-flow/cli/dist/src/services/managed-memory-worker.d.ts +56 -0
  20. package/v3/@claude-flow/cli/dist/src/services/managed-memory-worker.js +161 -0
  21. package/v3/@claude-flow/cli/dist/src/services/managed-run-collector.d.ts +93 -0
  22. package/v3/@claude-flow/cli/dist/src/services/managed-run-collector.js +181 -0
  23. package/v3/@claude-flow/cli/dist/src/services/managed-run-store.d.ts +52 -0
  24. package/v3/@claude-flow/cli/dist/src/services/managed-run-store.js +119 -0
  25. package/v3/@claude-flow/cli/dist/src/services/managed-workflows.d.ts +101 -0
  26. package/v3/@claude-flow/cli/dist/src/services/managed-workflows.js +183 -0
  27. 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
- const API_BASE = process.env.ANTHROPIC_BASE_URL?.replace(/\/$/, '') || 'https://api.anthropic.com';
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(`${API_BASE}/v1${path}`, {
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 (e) {
55
- return { ok: false, status: 0, error: `network error calling ${method} /v1${path}: ${e.message}` };
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
- let assistantText = '';
74
- const toolUses = [];
75
- let status = 'unknown';
76
- let stopReason = null;
77
- for (const e of events) {
78
- if (e.type === 'agent.message' && Array.isArray(e.content)) {
79
- assistantText += e.content.filter(b => b.type === 'text').map(b => b.text ?? '').join('');
80
- }
81
- else if (e.type === 'agent.tool_use') {
82
- toolUses.push({ name: e.name ?? '?', input: e.input });
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 { assistantText, toolUses, status, stopReason, eventCount: events.length };
140
+ return `scope-${scopeGeneration}`;
96
141
  }
97
- async function fetchSessionEvents(sessionId) {
98
- const r = await maRequest('GET', `/sessions/${encodeURIComponent(sessionId)}/events`);
99
- if (!r.ok)
100
- return [];
101
- const d = r.data;
102
- return (d.data ?? d.events ?? []);
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
- // Wait until the session is no longer "running": poll the event log for a
105
- // terminal session.status_* event. Bounded by maxWaitMs.
106
- async function waitForIdle(sessionId, maxWaitMs) {
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
- let events = [];
179
+ const progress = new ManagedProgress();
180
+ let started = false;
109
181
  while (Date.now() < deadline) {
110
- events = await fetchSessionEvents(sessionId);
111
- const terminal = events.some(e => e.type === 'session.status_idle' || e.type === 'session.status_error' || e.type === 'session.status_failed');
112
- // Also stop early if the session record itself reports a non-running status
113
- if (terminal)
114
- return { done: true, events };
115
- await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
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 { done: false, events };
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: 'Environment networking (default: unrestricted)' },
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: { type: net } },
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
- return { success: false, stage: 'environment', agentId: a.data.id, error: e.error };
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
- return { success: false, stage: 'session', agentId: a.data.id, environmentId: e.data.id, error: s.error };
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
- note: 'Cloud agent provisioned. Send work with managed_agent_prompt({sessionId, message}); inspect with managed_agent_events; clean up with managed_agent_terminate (it bills container time + tokens until you do).',
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
- const send = await maRequest('POST', `/sessions/${encodeURIComponent(sessionId)}/events`, {
223
- events: [{ type: 'user.message', content: [{ type: 'text', text: message }] }],
224
- });
225
- if (!send.ok)
226
- return { success: false, stage: 'send', sessionId, error: send.error };
227
- const maxWait = typeof input.maxWaitMs === 'number' && input.maxWaitMs > 0 ? input.maxWaitMs : DEFAULT_MAX_WAIT_MS;
228
- const { done, events } = await waitForIdle(sessionId, maxWait);
229
- const sum = summarizeEvents(events);
230
- return {
231
- success: true,
232
- runtime: 'managed',
233
- sessionId,
234
- finished: done,
235
- status: sum.status,
236
- stopReason: sum.stopReason,
237
- assistantText: sum.assistantText,
238
- toolUses: sum.toolUses,
239
- eventCount: sum.eventCount,
240
- note: done
241
- ? undefined
242
- : `Session still running after ${maxWait}ms — call managed_agent_events({sessionId}) to keep watching, or managed_agent_prompt again to steer.`,
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
- const events = await fetchSessionEvents(sessionId);
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.type === 'text').map(b => b.text ?? '').join('').slice(0, 500) };
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
- status: sum.status,
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 keeps billing container time + tokens until deleted. 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.',
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 (typeof input.environmentId === 'string' && input.environmentId) {
347
- const e = await maRequest('DELETE', `/environments/${encodeURIComponent(String(input.environmentId))}`);
348
- result.environmentDeleted = e.ok;
349
- if (!e.ok)
350
- result.environmentError = e.error;
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
  },
@@ -0,0 +1,4 @@
1
+ /** Learning MCP boundary, kept separate from provider transport and native workers. */
2
+ import { type MCPTool } from './types.js';
3
+ export declare const managedLearningTool: Pick<MCPTool, 'inputSchema' | 'handler'>;
4
+ //# sourceMappingURL=managed-learning-tool.d.ts.map