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.
- package/.claude/.proven-config-version +1 -0
- package/.claude/helpers/.helpers-version +1 -0
- package/.claude/helpers/helpers.manifest.json +13 -0
- package/.claude/proven-config.json +42 -0
- package/.claude/settings.local.json +8 -0
- package/.claude-plugin/marketplace.json +3 -3
- package/README.md +2 -2
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
- package/package.json +1 -1
- package/v3/@claude-flow/cli/README.md +2 -2
- package/v3/@claude-flow/cli/catalog-manifest.json +3 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-tools.js +7 -2
- 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 +347 -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/ruvector/diskann-backend.d.ts +78 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/diskann-backend.js +310 -0
- package/v3/@claude-flow/cli/dist/src/services/flywheel-transaction.js +5 -0
- 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
- package/v3/@claude-flow/shared/dist/events/event-store.test.d.ts +8 -0
- package/v3/@claude-flow/shared/dist/events/event-store.test.js +293 -0
- package/v3/@claude-flow/shared/dist/hooks/hooks.test.d.ts +9 -0
- package/v3/@claude-flow/shared/dist/hooks/hooks.test.js +322 -0
- package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.d.ts +9 -0
- 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
|
-
|
|
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
|
-
|
|
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(`${
|
|
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
|
|
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 };
|
|
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
|
-
|
|
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';
|
|
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 {
|
|
143
|
+
return `scope-${scopeGeneration}`;
|
|
96
144
|
}
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
182
|
+
const progress = new ManagedProgress();
|
|
183
|
+
let started = false;
|
|
109
184
|
while (Date.now() < deadline) {
|
|
110
|
-
events = await
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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: '
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
},
|