claude-flow 3.62.0 → 3.63.1

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 (46) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/.helpers-version +1 -0
  3. package/.claude/helpers/helpers.manifest.json +13 -0
  4. package/.claude/proven-config.json +42 -0
  5. package/.claude/settings.local.json +8 -0
  6. package/.claude-plugin/marketplace.json +3 -3
  7. package/README.md +2 -2
  8. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  9. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  10. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  11. package/package.json +1 -1
  12. package/v3/@claude-flow/cli/README.md +2 -2
  13. package/v3/@claude-flow/cli/catalog-manifest.json +3 -3
  14. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-tools.js +7 -2
  15. package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.d.ts +1 -20
  16. package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.js +347 -108
  17. package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-learning-tool.d.ts +4 -0
  18. package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-learning-tool.js +46 -0
  19. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +6 -3
  20. package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +12 -12
  21. package/v3/@claude-flow/cli/dist/src/ruvector/diskann-backend.d.ts +78 -0
  22. package/v3/@claude-flow/cli/dist/src/ruvector/diskann-backend.js +310 -0
  23. package/v3/@claude-flow/cli/dist/src/services/flywheel-transaction.js +5 -0
  24. package/v3/@claude-flow/cli/dist/src/services/managed-learning-loop.d.ts +79 -0
  25. package/v3/@claude-flow/cli/dist/src/services/managed-learning-loop.js +76 -0
  26. package/v3/@claude-flow/cli/dist/src/services/managed-learning-store.d.ts +187 -0
  27. package/v3/@claude-flow/cli/dist/src/services/managed-learning-store.js +238 -0
  28. package/v3/@claude-flow/cli/dist/src/services/managed-learning.d.ts +108 -0
  29. package/v3/@claude-flow/cli/dist/src/services/managed-learning.js +197 -0
  30. package/v3/@claude-flow/cli/dist/src/services/managed-memory-client.d.ts +72 -0
  31. package/v3/@claude-flow/cli/dist/src/services/managed-memory-client.js +236 -0
  32. package/v3/@claude-flow/cli/dist/src/services/managed-memory-worker.d.ts +56 -0
  33. package/v3/@claude-flow/cli/dist/src/services/managed-memory-worker.js +161 -0
  34. package/v3/@claude-flow/cli/dist/src/services/managed-run-collector.d.ts +93 -0
  35. package/v3/@claude-flow/cli/dist/src/services/managed-run-collector.js +181 -0
  36. package/v3/@claude-flow/cli/dist/src/services/managed-run-store.d.ts +52 -0
  37. package/v3/@claude-flow/cli/dist/src/services/managed-run-store.js +119 -0
  38. package/v3/@claude-flow/cli/dist/src/services/managed-workflows.d.ts +101 -0
  39. package/v3/@claude-flow/cli/dist/src/services/managed-workflows.js +183 -0
  40. package/v3/@claude-flow/cli/package.json +1 -1
  41. package/v3/@claude-flow/shared/dist/events/event-store.test.d.ts +8 -0
  42. package/v3/@claude-flow/shared/dist/events/event-store.test.js +293 -0
  43. package/v3/@claude-flow/shared/dist/hooks/hooks.test.d.ts +9 -0
  44. package/v3/@claude-flow/shared/dist/hooks/hooks.test.js +322 -0
  45. package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.d.ts +9 -0
  46. package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.js +93 -0
@@ -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,168 @@ 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;
57
+ // encodeURIComponent leaves '.' and '..' alone, so an id of '..' would walk up the API path (/v1/sessions/.. is /v1/).
58
+ if (path.split('?')[0].split('/').some(segment => segment === '.' || segment === '..'))
59
+ return { ok: false, status: 0, error: 'Invalid managed resource id' };
46
60
  try {
47
- res = await fetch(`${API_BASE}/v1${path}`, {
48
- method,
49
- headers: headers(key),
61
+ const res = await fetch(`${apiBase()}/v1${path}`, {
62
+ method, headers: headers(key), redirect: 'error',
50
63
  body: body !== undefined ? JSON.stringify(body) : undefined,
51
- signal: AbortSignal.timeout(30_000),
64
+ signal: AbortSignal.timeout(Math.max(1, Math.min(timeoutMs, 30_000))),
52
65
  });
66
+ // Never echo upstream bodies: they may contain prompts, keys or MCP tokens.
67
+ if (!res.ok) {
68
+ await res.body?.cancel();
69
+ return { ok: false, status: res.status, error: `Managed Agents API HTTP ${res.status}` };
70
+ }
71
+ if (method === 'DELETE' && ![200, 204].includes(res.status)) {
72
+ await res.body?.cancel();
73
+ return { ok: false, status: res.status, error: 'Provider deletion is not yet confirmed' };
74
+ }
75
+ if (res.status === 204)
76
+ return { ok: true, status: res.status, data: undefined };
77
+ const reader = res.body?.getReader();
78
+ const chunks = [];
79
+ let bytes = 0;
80
+ if (reader) {
81
+ try {
82
+ while (true) {
83
+ const chunk = await reader.read();
84
+ if (chunk.done)
85
+ break;
86
+ bytes += chunk.value.byteLength;
87
+ if (bytes > 8_000_000) {
88
+ await reader.cancel();
89
+ throw new Error('response limit');
90
+ }
91
+ chunks.push(chunk.value);
92
+ }
93
+ }
94
+ finally {
95
+ reader.releaseLock();
96
+ }
97
+ }
98
+ const text = Buffer.concat(chunks).toString('utf8');
99
+ return { ok: true, status: res.status, data: (text ? JSON.parse(text) : undefined) };
53
100
  }
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 };
101
+ catch {
102
+ return { ok: false, status: 0, error: 'Managed Agents transport or response error; inspect provider status before retrying mutations' };
68
103
  }
69
- return { ok: true, status: res.status, data: parsed };
70
104
  }
71
- // ---- helpers --------------------------------------------------------------
72
105
  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';
106
+ const progress = new ManagedProgress();
107
+ progress.accept(events);
108
+ return progress.snapshot();
109
+ }
110
+ async function fetchSessionEvents(sessionId, since, threadId, deadline = Date.now() + 30_000, expectedScope = currentCollectorScope()) {
111
+ return readManagedEvents(async (query) => {
112
+ const thread = threadId ? `/threads/${encodeURIComponent(threadId)}` : '';
113
+ const params = new URLSearchParams(query);
114
+ if (threadId) {
115
+ params.delete('order');
116
+ params.delete('created_at[gte]');
93
117
  }
118
+ if (currentCollectorScope() !== expectedScope)
119
+ throw new Error('Managed credential scope changed during observation');
120
+ if (Date.now() >= deadline)
121
+ throw new Error('Managed Agents observation deadline reached');
122
+ const r = await maRequest('GET', `/sessions/${encodeURIComponent(sessionId)}${thread}/events?${params}`, undefined, deadline - Date.now());
123
+ if (!r.ok)
124
+ throw new Error(r.error);
125
+ if (currentCollectorScope() !== expectedScope)
126
+ throw new Error('Managed credential scope changed during observation');
127
+ return r.data;
128
+ }, since);
129
+ }
130
+ let collectorScope = '';
131
+ let collector = new ManagedRunCollector();
132
+ // The scope only answers "did the project, endpoint or credential change since this collector was built". It never leaves the
133
+ // process, so it is a generation number, not a digest: the API key is compared in memory (it already lives in the environment)
134
+ // and is never fed to a hash function. Returning to an earlier value counts as a change, which is the safe direction.
135
+ let scopeInput = '';
136
+ let scopeGeneration = 0;
137
+ function currentCollectorScope() {
138
+ const input = JSON.stringify([getProjectCwd(), apiBase(), apiKey()]);
139
+ if (input !== scopeInput) {
140
+ scopeInput = input;
141
+ scopeGeneration++;
94
142
  }
95
- return { assistantText, toolUses, status, stopReason, eventCount: events.length };
143
+ return `scope-${scopeGeneration}`;
96
144
  }
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 ?? []);
145
+ function scopedCollector() {
146
+ const scope = currentCollectorScope();
147
+ if (scope !== collectorScope) {
148
+ collector = new ManagedRunCollector();
149
+ collectorScope = scope;
150
+ }
151
+ return collector;
103
152
  }
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) {
153
+ async function observeSession(sessionId, deadline = Date.now() + 30_000) {
154
+ const root = getProjectCwd(), active = scopedCollector(), expectedScope = collectorScope;
155
+ return collectWithLease(root, sessionId, async () => {
156
+ const result = await active.refresh(sessionId, {
157
+ readEvents: since => fetchSessionEvents(sessionId, since, undefined, deadline, expectedScope),
158
+ readSession: async () => {
159
+ if (currentCollectorScope() !== expectedScope)
160
+ throw new Error('Managed credential scope changed during observation');
161
+ const r = await maRequest('GET', `/sessions/${encodeURIComponent(sessionId)}`, undefined, deadline - Date.now());
162
+ if (!r.ok)
163
+ throw new Error(r.error);
164
+ return r.data;
165
+ },
166
+ });
167
+ if (currentCollectorScope() !== expectedScope)
168
+ throw new Error('Managed credential scope changed during observation');
169
+ // The snapshot only feeds the console. A write failure (size cap, stale lock) must not fail a successful provider read.
170
+ try {
171
+ publishManagedSnapshot(root, result.snapshot);
172
+ }
173
+ catch { /* best effort */ }
174
+ return result;
175
+ }, expectedScope);
176
+ }
177
+ // Gate new-turn observations on the provider's acknowledged message ID, not
178
+ // on a local timestamp or a historical idle event. Calls are serialized locally.
179
+ const pendingPrompts = new Set();
180
+ async function waitForIdle(sessionId, maxWaitMs, messageId) {
107
181
  const deadline = Date.now() + Math.max(1_000, Math.min(maxWaitMs, 600_000));
108
- let events = [];
182
+ const progress = new ManagedProgress();
183
+ let started = false;
109
184
  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));
185
+ const { events } = await observeSession(sessionId, deadline);
186
+ if (!started) {
187
+ const boundary = events.findIndex(e => e.id === messageId && !!e.processed_at);
188
+ if (boundary >= 0) {
189
+ // Existing runs remain relevant when a user steers an active session.
190
+ const prior = new ManagedProgress();
191
+ const history = events.slice(0, boundary).filter(e => e.type.startsWith('workflow_run.'));
192
+ prior.accept(history);
193
+ const open = new Set(prior.snapshot().workflowRuns.filter(r => r.status !== 'ended').map(r => r.id));
194
+ progress.accept(history.filter(e => e.workflow_run_id && open.has(e.workflow_run_id)));
195
+ progress.accept(events.slice(boundary));
196
+ started = true;
197
+ }
198
+ }
199
+ else {
200
+ // The provider returns the whole session history on every read. Keep ignoring everything before the
201
+ // acknowledged message: its old end_turn idle would otherwise be applied on a later poll and report
202
+ // the previous turn's answer as finished while the new turn is still running.
203
+ const boundary = events.findIndex(e => e.id === messageId);
204
+ if (boundary >= 0)
205
+ progress.accept(events.slice(boundary));
206
+ }
207
+ const state = progress.snapshot();
208
+ if (started && (state.finished || state.blocked || (state.interrupted && state.status === 'idle') || state.status === 'terminated'))
209
+ return state;
210
+ await new Promise(r => setTimeout(r, Math.min(POLL_INTERVAL_MS, Math.max(0, deadline - Date.now()))));
116
211
  }
117
- return { done: false, events };
212
+ return progress.snapshot();
118
213
  }
214
+ function definiteRejection(status) { return [400, 401, 403, 404, 422].includes(status); }
119
215
  function maName(prefix, given) {
120
216
  if (typeof given === 'string' && given.trim())
121
217
  return given.trim().slice(0, 80);
@@ -123,9 +219,62 @@ function maName(prefix, given) {
123
219
  }
124
220
  // ---------------------------------------------------------------------------
125
221
  export const managedAgentTools = [
222
+ {
223
+ name: 'managed_agent_capabilities',
224
+ 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.',
225
+ category: 'agent',
226
+ inputSchema: { type: 'object', properties: {} },
227
+ handler: async () => ({
228
+ runtime: 'managed', provider: 'anthropic', networkCalled: false,
229
+ credentialConfigured: !!apiKey(), betaAccess: 'unknown', remoteHealth: 'unknown', authorization: 'host-policy-required',
230
+ features: ['dynamic-workflows', 'budget-at-creation', 'limited-networking', 'predefined-agents', 'thread-inspection', 'interrupt-request', 'session-delete', 'verified-learning'],
231
+ create: { dynamicWorkflows: true, budgetCents: 'positive integer US cents string', networking: 'limited', predefinedAgents: 'optional agent IDs; required by default mod policy' },
232
+ controls: { budgetOvershoot: 'in-flight requests may finish', promptTimeout: 'observation only', interrupt: 'pause requested, not cancellation', verification: 'unverified' },
233
+ 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' },
234
+ learning: { enabled: process.env.RUFLO_MANAGED_LEARNING === '1', verifierConfigured: !!process.env.RUFLO_MANAGED_VERIFIER_PUBLIC_KEY, defaultsChanged: false },
235
+ scope: 'Adapter facts do not grant tools, credentials, plugins or release authority. Mod admission is host-session-local, not global spend enforcement.',
236
+ }),
237
+ },
238
+ {
239
+ name: 'managed_agent_learning',
240
+ 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.',
241
+ category: 'agent', inputSchema: managedLearningTool.inputSchema, handler: managedLearningTool.handler,
242
+ },
243
+ {
244
+ name: 'managed_agent_control',
245
+ 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.',
246
+ category: 'agent',
247
+ inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, action: { type: 'string', enum: ['inspect', 'interrupt', 'terminate'] }, threadOffset: { type: 'integer', minimum: 0, maximum: 9999 } }, required: ['sessionId', 'action'] },
248
+ handler: async (input) => {
249
+ if (!apiKey())
250
+ return { success: false, ...NEEDS_KEY };
251
+ try {
252
+ const request = async (method, id, body) => {
253
+ const r = await maRequest(method, `/sessions/${encodeURIComponent(id)}${method === 'POST' ? '/events' : ''}`, body);
254
+ if (!r.ok)
255
+ throw new Error(r.error);
256
+ return r.data;
257
+ };
258
+ return await executeManagedControl(input, {
259
+ policy: () => ({ enabled: process.env.RUFLO_MANAGED_CONTROL === '1', sessions: (process.env.RUFLO_MANAGED_CONTROL_SESSIONS ?? '').split(',').map(s => s.trim()).filter(Boolean) }),
260
+ read: id => request('GET', id),
261
+ inspect: async (id, offset) => { await observeSession(id); const page = scopedCollector().page(id, offset); if (!page)
262
+ throw new Error('Snapshot unavailable'); publishManagedSnapshot(getProjectCwd(), page); return page; },
263
+ interrupt: id => request('POST', id, { events: [{ type: 'user.interrupt' }] }),
264
+ terminate: async (id) => { const result = await request('DELETE', id); try {
265
+ publishManagedTermination(getProjectCwd(), id, scopedCollector());
266
+ }
267
+ catch { /* the provider deletion is the fact; the local snapshot is best-effort */ } return result; },
268
+ });
269
+ }
270
+ catch {
271
+ return { success: false, error: 'Managed action refused: recheck current host policy, provider state and observation lease' };
272
+ }
273
+ },
274
+ },
126
275
  {
127
276
  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.',
277
+ 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
278
  category: 'agent',
130
279
  inputSchema: {
131
280
  type: 'object',
@@ -141,13 +290,24 @@ export const managedAgentTools = [
141
290
  },
142
291
  skills: { type: 'array', description: 'Skills to attach to the agent', items: { type: 'object' } },
143
292
  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)' },
293
+ networking: { type: 'string', enum: ['unrestricted', 'restricted', 'none', 'limited'], description: 'Legacy default unrestricted; dynamic workflows require limited' },
294
+ dynamicWorkflows: { type: 'boolean', description: 'Opt in to multiagent_20261001 dynamic workflows (requires budgetCents)' },
295
+ budgetCents: { type: 'string', pattern: '^[1-9][0-9]{0,8}$', description: 'Positive integer US cents string; 2500 means $25; in-flight requests may overshoot' },
296
+ allowedHosts: { type: 'array', items: { type: 'string' }, description: 'Exact allowed hostnames for dynamic workflow limited networking' },
297
+ predefinedAgents: { type: 'array', minItems: 1, maxItems: 20, items: { type: 'string' }, description: 'Restrict workflows to these agent IDs; disables inline agents' },
145
298
  initScript: { type: 'string', description: 'Environment init script (bash, run at container start)' },
146
299
  },
147
300
  },
148
301
  handler: async (input) => {
149
302
  if (!apiKey())
150
303
  return { success: false, ...NEEDS_KEY };
304
+ let config;
305
+ try {
306
+ config = dynamicConfig(input);
307
+ }
308
+ catch (error) {
309
+ return { success: false, error: error.message };
310
+ }
151
311
  // 1. Agent
152
312
  const agentBody = {
153
313
  name: maName('ruflo-managed', input.name),
@@ -156,34 +316,51 @@ export const managedAgentTools = [
156
316
  };
157
317
  if (typeof input.system === 'string')
158
318
  agentBody.system = input.system;
319
+ if (config.multiagent)
320
+ agentBody.multiagent = config.multiagent;
159
321
  if (Array.isArray(input.mcpServers) && input.mcpServers.length)
160
322
  agentBody.mcp_servers = input.mcpServers;
161
323
  if (Array.isArray(input.skills) && input.skills.length)
162
324
  agentBody.skills = input.skills;
163
325
  const a = await maRequest('POST', '/agents', agentBody);
164
326
  if (!a.ok)
165
- return { success: false, stage: 'agent', error: a.error };
327
+ return { success: false, stage: 'agent', error: a.error, cleanupRequired: !definiteRejection(a.status) };
328
+ if (!a.data || typeof a.data.id !== 'string' || !/^agent_[A-Za-z0-9]+$/.test(a.data.id))
329
+ return { success: false, stage: 'agent', error: 'Invalid agent acknowledgement; inspect provider resources before retrying', cleanupRequired: true };
166
330
  // 2. Environment
167
- const net = input.networking || 'unrestricted';
168
331
  const envBody = {
169
332
  name: maName('ruflo-managed-env', input.name),
170
- config: { type: 'cloud', networking: { type: net } },
333
+ config: { type: 'cloud', networking: config.networking },
171
334
  };
172
335
  if (input.packages && typeof input.packages === 'object')
173
336
  envBody.config.packages = { type: 'packages', ...input.packages };
174
337
  if (typeof input.initScript === 'string' && input.initScript)
175
338
  envBody.config.init_script = input.initScript;
176
339
  const e = await maRequest('POST', '/environments', envBody);
177
- if (!e.ok)
178
- return { success: false, stage: 'environment', agentId: a.data.id, error: e.error };
340
+ if (!e.ok) {
341
+ if (!definiteRejection(e.status))
342
+ return { success: false, stage: 'environment', agentId: a.data.id, error: e.error, cleanupRequired: true };
343
+ const cleanup = await maRequest('DELETE', `/agents/${encodeURIComponent(a.data.id)}`);
344
+ return { success: false, stage: 'environment', agentId: a.data.id, error: e.error, agentDeleted: cleanup.ok };
345
+ }
346
+ if (!e.data || typeof e.data.id !== 'string' || !/^env_[A-Za-z0-9]+$/.test(e.data.id))
347
+ return { success: false, stage: 'environment', agentId: a.data.id, error: 'Invalid environment acknowledgement; inspect provider resources before retrying', cleanupRequired: true };
179
348
  // 3. Session
180
349
  const s = await maRequest('POST', '/sessions', {
181
350
  agent: a.data.id,
182
351
  environment_id: e.data.id,
183
352
  title: maName('ruflo-managed session', input.title),
353
+ ...(config.budget ? { budget: config.budget } : {}),
184
354
  });
185
- if (!s.ok)
186
- return { success: false, stage: 'session', agentId: a.data.id, environmentId: e.data.id, error: s.error };
355
+ if (!s.ok) {
356
+ if (!definiteRejection(s.status))
357
+ return { success: false, stage: 'session', agentId: a.data.id, environmentId: e.data.id, error: s.error, cleanupRequired: true };
358
+ const envCleanup = await maRequest('DELETE', `/environments/${encodeURIComponent(e.data.id)}`);
359
+ const agentCleanup = await maRequest('DELETE', `/agents/${encodeURIComponent(a.data.id)}`);
360
+ return { success: false, stage: 'session', agentId: a.data.id, environmentId: e.data.id, error: s.error, environmentDeleted: envCleanup.ok, agentDeleted: agentCleanup.ok };
361
+ }
362
+ if (!s.data || typeof s.data.id !== 'string' || !/^sesn_[A-Za-z0-9]+$/.test(s.data.id))
363
+ 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
364
  return {
188
365
  success: true,
189
366
  runtime: 'managed',
@@ -193,7 +370,10 @@ export const managedAgentTools = [
193
370
  environmentId: e.data.id,
194
371
  status: s.data.status ?? 'idle',
195
372
  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).',
373
+ dynamicWorkflows: config.enabled,
374
+ budget: config.budget,
375
+ verification: 'unverified',
376
+ 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
377
  };
198
378
  },
199
379
  },
@@ -219,28 +399,38 @@ export const managedAgentTools = [
219
399
  return { success: false, error: 'sessionId is required' };
220
400
  if (!message)
221
401
  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
- };
402
+ if (input.maxWaitMs !== undefined && (typeof input.maxWaitMs !== 'number' || !Number.isFinite(input.maxWaitMs) || input.maxWaitMs <= 0))
403
+ return { success: false, error: 'maxWaitMs must be finite and positive' };
404
+ if (pendingPrompts.has(sessionId))
405
+ return { success: false, error: 'A prompt is already pending for this session' };
406
+ pendingPrompts.add(sessionId);
407
+ let sentMessageId;
408
+ try {
409
+ const send = await maRequest('POST', `/sessions/${encodeURIComponent(sessionId)}/events`, {
410
+ events: [{ type: 'user.message', content: [{ type: 'text', text: message }] }],
411
+ });
412
+ if (!send.ok)
413
+ return { success: false, stage: 'send', sessionId, error: send.error };
414
+ const sent = eventPage(send.data).data.find(e => e.type === 'user.message');
415
+ if (!sent)
416
+ return { success: false, sessionId, error: 'Missing message acknowledgement; inspect events before retrying' };
417
+ sentMessageId = sent.id;
418
+ const maxWait = typeof input.maxWaitMs === 'number' ? input.maxWaitMs : DEFAULT_MAX_WAIT_MS;
419
+ const state = await waitForIdle(sessionId, maxWait, sent.id);
420
+ return {
421
+ success: state.observedFailures === 0 && state.status !== 'terminated',
422
+ runtime: 'managed', sessionId, messageId: sent.id, ...state,
423
+ 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.',
424
+ };
425
+ }
426
+ catch (error) {
427
+ // Once the provider acknowledged the message, say so: a retry would send it twice.
428
+ return { success: false, sessionId, finished: false, error: error.message,
429
+ ...(sentMessageId ? { messageId: sentMessageId, note: 'The message was already sent; inspect events instead of resending.' } : {}) };
430
+ }
431
+ finally {
432
+ pendingPrompts.delete(sessionId);
433
+ }
244
434
  },
245
435
  },
246
436
  {
@@ -268,6 +458,7 @@ export const managedAgentTools = [
268
458
  type: 'object',
269
459
  properties: {
270
460
  sessionId: { type: 'string', description: 'Session id' },
461
+ threadId: { type: 'string', description: 'Optional child thread ID from session.thread_created; inspect worker failures' },
271
462
  raw: { type: 'boolean', description: 'Include the full raw event objects (default: summary + compact list)' },
272
463
  },
273
464
  required: ['sessionId'],
@@ -278,25 +469,27 @@ export const managedAgentTools = [
278
469
  const sessionId = String(input.sessionId ?? '');
279
470
  if (!sessionId)
280
471
  return { error: 'sessionId is required' };
281
- const events = await fetchSessionEvents(sessionId);
472
+ let events;
473
+ try {
474
+ events = typeof input.threadId === 'string' ? await fetchSessionEvents(sessionId, undefined, input.threadId) : (await observeSession(sessionId)).events;
475
+ }
476
+ catch (error) {
477
+ return { success: false, sessionId, error: error.message };
478
+ }
282
479
  const sum = summarizeEvents(events);
283
480
  const compact = events.map(e => {
284
481
  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) };
482
+ return { type: e.type, text: (e.content ?? []).filter(b => b?.type === 'text').map(b => b.text ?? '').join('').slice(0, 500) };
286
483
  if (e.type === 'agent.tool_use')
287
484
  return { type: e.type, name: e.name, input: e.input };
288
485
  if (e.type === 'agent.tool_result')
289
486
  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 } : {}) };
487
+ 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
488
  });
292
489
  return {
293
490
  runtime: 'managed',
294
491
  sessionId,
295
- status: sum.status,
296
- stopReason: sum.stopReason,
297
- assistantText: sum.assistantText,
298
- toolUses: sum.toolUses,
299
- eventCount: sum.eventCount,
492
+ ...sum,
300
493
  events: input.raw ? events : compact,
301
494
  };
302
495
  },
@@ -321,9 +514,26 @@ export const managedAgentTools = [
321
514
  };
322
515
  },
323
516
  },
517
+ {
518
+ name: 'managed_agent_interrupt',
519
+ 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.',
520
+ category: 'agent',
521
+ inputSchema: { type: 'object', properties: { sessionId: { type: 'string', description: 'Managed session to interrupt' } }, required: ['sessionId'] },
522
+ handler: async (input) => {
523
+ if (!apiKey())
524
+ return { success: false, ...NEEDS_KEY };
525
+ if (typeof input.sessionId !== 'string' || !input.sessionId)
526
+ return { success: false, error: 'sessionId is required' };
527
+ // No ungated path to a remote action: this uses the same host policy, session allowlist and fresh provider-state
528
+ // check as managed_agent_control, so a prompt-injected caller cannot interrupt arbitrary organisation sessions.
529
+ const control = managedAgentTools.find(t => t.name === 'managed_agent_control');
530
+ const r = await control.handler({ sessionId: input.sessionId, action: 'interrupt' });
531
+ return { ...r, sessionId: input.sessionId, interruptRequested: r.success === true, cancellationConfirmed: false };
532
+ },
533
+ },
324
534
  {
325
535
  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.',
536
+ 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
537
  category: 'agent',
328
538
  inputSchema: {
329
539
  type: 'object',
@@ -339,17 +549,46 @@ export const managedAgentTools = [
339
549
  const sessionId = String(input.sessionId ?? '');
340
550
  if (!sessionId)
341
551
  return { success: false, error: 'sessionId is required' };
552
+ // Default behaviour is unchanged. Once the host has switched on the governed control plane
553
+ // (RUFLO_MANAGED_CONTROL=1) this tool obeys the same session allowlist, and may delete only that session's own environment.
554
+ const governed = process.env.RUFLO_MANAGED_CONTROL === '1';
555
+ if (governed && !(process.env.RUFLO_MANAGED_CONTROL_SESSIONS ?? '').split(',').map(x => x.trim()).filter(Boolean).includes(sessionId)) {
556
+ return { success: false, error: 'Host managed control policy denies this session' };
557
+ }
558
+ const wantsEnvironment = typeof input.environmentId === 'string' && input.environmentId.length > 0;
559
+ let ownEnvironment;
560
+ if (governed && wantsEnvironment) {
561
+ const g = await maRequest('GET', `/sessions/${encodeURIComponent(sessionId)}`);
562
+ if (!g.ok)
563
+ return { success: false, error: g.error };
564
+ ownEnvironment = g.data.environment_id;
565
+ }
342
566
  const s = await maRequest('DELETE', `/sessions/${encodeURIComponent(sessionId)}`);
343
567
  const result = { runtime: 'managed', sessionId, sessionDeleted: s.ok };
568
+ // The deletion already happened; a failed local snapshot update must not turn it into an error or skip the environment.
569
+ if (s.ok) {
570
+ try {
571
+ publishManagedTermination(getProjectCwd(), sessionId, scopedCollector());
572
+ }
573
+ catch {
574
+ result.snapshotUpdated = false;
575
+ }
576
+ }
344
577
  if (!s.ok)
345
578
  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;
579
+ if (s.ok && wantsEnvironment) {
580
+ if (governed && input.environmentId !== ownEnvironment) {
581
+ result.environmentDeleted = false;
582
+ result.environmentError = 'environmentId does not belong to this session';
583
+ }
584
+ else {
585
+ const e = await maRequest('DELETE', `/environments/${encodeURIComponent(String(input.environmentId))}`);
586
+ result.environmentDeleted = e.ok;
587
+ if (!e.ok)
588
+ result.environmentError = e.error;
589
+ }
351
590
  }
352
- result.success = s.ok;
591
+ result.success = s.ok && result.environmentDeleted !== false;
353
592
  return result;
354
593
  },
355
594
  },
@@ -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