Files
ruvnet__ruflo/v3/@claude-flow/cli/__tests__/managed-agent-tools.test.ts
rUv ef73a1616f feat(ruflo-agent): rename ruflo-wasm → ruflo-agent + Claude Managed Agents (cloud) runtime — ADR-115 (proposed→accepted, implemented) (#1929)
* docs(adr): ADR-115 — Claude Managed Agents as a cloud backend for rvagent (explored + validated)

Records the proposal to add `runtime: "managed"` to ruflo's rvagent
interface (ruflo-wasm / @ruvector/rvagent-wasm / wasm_agent_* MCP tools),
backed by Anthropic's Claude Managed Agents API (beta), as a second backend
alongside the local WASM sandbox — mirroring the ConsensusTransport
LocalTransport|FederationTransport pattern (ADR-095 G2 / ADR-104). The
agent/environment/session/events model maps ~1:1 to wasm_agent_create/
prompt/files/terminate + the gallery.

Validated live against api.anthropic.com/v1 with the org's Anthropic key
(GCP claude-flow/anthropic-api-key): created an agent + environment + session,
sent a bash task, observed the full event log (status_running → user.message
→ tool_use(bash) → tool_result → agent.message("Done.") → status_idle,
stop_reason end_turn), cleaned up. ~7s for a trivial Haiku task.

High-value combos noted: ruflo's own MCP server as a Managed Agent tool
source (cloud-hosted ruflo, zero local infra); Managed Agent session as a
third task_assign executor (closes #1916 items 3/4); long-running/async work;
skills + define-outcomes parity. Constraints: opt-in/off-by-default, needs
ANTHROPIC_API_KEY + beta access, plugin-level @anthropic-ai/sdk dep,
cost-tracking + orphaned-session GC, beta churn, branding.

Status: proposed. Registered in AgentDB (adr-ADR-115) + adr-patterns.

Co-Authored-By: RuFlo <ruv@ruv.net>

* refactor(plugins): rename ruflo-wasm → ruflo-agent (agent-runtimes home; ADR-115 prep)

`ruflo-wasm` only covered local WASM-sandboxed agents (rvagent —
@ruvector/rvagent-wasm, the wasm_agent_*/wasm_gallery_* MCP tools). It's
becoming the home for *all* ruflo agent runtimes: the existing local-WASM
backend plus Anthropic's Claude Managed Agents as a cloud backend (ADR-115),
behind one interface — same local-vs-cloud pattern as ConsensusTransport.

- git mv plugins/ruflo-wasm → plugins/ruflo-agent
- plugin.json: name ruflo-wasm → ruflo-agent; description/keywords now span
  both runtimes (rvagent/wasm + managed-agents/anthropic/cloud)
- .claude-plugin/marketplace.json entry renamed
- README / plugins/README / docs/index / cli README / ruflo README /
  discover-plugins SKILL / verification/inventory.json / ADR-115 — refs updated
- wasm_agent_* / wasm_gallery_* MCP tool names are unchanged (they stay; the
  plugin is just renamed). managed_agent_* tools land next per ADR-115.

(Left `ruflo-wasm-mcp` in ruvocal/workerClient.ts alone — unrelated worker MCP name.)

Co-Authored-By: RuFlo <ruv@ruv.net>

* feat(cli): managed_agent_* MCP tools — Claude Managed Agents cloud runtime (ADR-115)

Six new MCP tools wrapping Anthropic's Claude Managed Agents REST API (beta,
`anthropic-beta: managed-agents-2026-04-01`) via plain fetch — no new SDK dep:

  managed_agent_create    agents.create + environments.create + sessions.create   ↔ wasm_agent_create (CLOUD counterpart)
  managed_agent_prompt    events.send(user.message) + poll events until idle       ↔ wasm_agent_prompt
  managed_agent_status    GET /v1/sessions/{id}
  managed_agent_events    GET /v1/sessions/{id}/events  (full transcript)           ↔ wasm_agent_files
  managed_agent_list      GET /v1/sessions
  managed_agent_terminate DELETE /v1/sessions/{id} (+ optionally the env)           ↔ wasm_agent_terminate

Needs ANTHROPIC_API_KEY (or CLAUDE_API_KEY); every tool degrades gracefully
with a structured "use wasm_agent_* for a local no-key runtime" error when the
key is absent — the CLI/MCP server stays up. ADR-112-compliant descriptions
(each says when wasm_agent_* / native is the right call instead). Registered
in mcp-client.ts. Lives conceptually in the `ruflo-agent` plugin (the
`ruflo-wasm`→`ruflo-agent` rename in the prior commit).

Validated live against api.anthropic.com with the org's Anthropic key (GCP
claude-flow/anthropic-api-key) via `ruflo mcp exec -t managed_agent_*`:
create (haiku) → prompt ("echo … > /tmp/r.txt && cat") → got finished:true,
status:idle, stopReason:end_turn, assistantText:"Done.", toolUses:[bash],
13 events → terminate (sessionDeleted+environmentDeleted). No-key path returns
the graceful error. audit-cli-mcp-tools 311 registered/0 dangling;
audit-tool-descriptions 0 no-guidance; cli build clean.

Co-Authored-By: RuFlo <ruv@ruv.net>

* feat(ruflo-agent): wire the managed-agent (cloud) runtime into the plugin + CI smoke

- skills/managed-agent/SKILL.md — drives the cloud runtime: managed_agent_create
  / prompt / status / events / list / terminate, with the WASM-vs-managed
  decision table, the ANTHROPIC_API_KEY prereq + graceful fallback, the
  mcpServers-reachability caveat, and the cost/cleanup discipline (a cloud
  session bills until terminated). Explicit allowed-tools (no wildcard).
- commands/managed-agent.md — quick "list cloud sessions / status / transcript
  / clean up" command (the cloud sibling of /wasm).
- plugin.json — kept rvagent-wasm + ruvllm-wasm keywords (still built on those);
  added managed-agents/anthropic/cloud.
- smoke.sh: step 1 now also asserts name=ruflo-agent + the managed-agents
  keywords; step 10 expects ADR-0001 status `Accepted` (was stale `Proposed`,
  pre-existing red); new step 12 — managed-agent skill+command exist + all 6
  managed_agent_* tools referenced + the skill keeps an explicit allowed-tools
  list + offers the wasm_agent_create fallback. 12 passed / 0 failed.
- .github/workflows/ruflo-agent-smoke.yml — new CI job running smoke.sh on any
  plugins/ruflo-agent/** change (the plugin had no CI gate before).

Validated: `claude --plugin-dir plugins/ruflo-agent` loads the plugin and lists
its skills (managed-agent, wasm-agent, wasm-gallery); plugin.json + all skill
frontmatters parse; smoke.sh 12/0.

Co-Authored-By: RuFlo <ruv@ruv.net>

* docs(adr): ADR-115 → Accepted + Implementation section (managed_agent_* landed)

Status Proposed → Accepted. Added an Implementation (2026-05-12) section:
the ruflo-wasm→ruflo-agent rename, the 6 managed_agent_* MCP tools (plain
fetch, no new SDK dep, graceful no-key degradation, ADR-112 descriptions),
the managed-agent skill+command, the smoke.sh/ruflo-agent-smoke.yml CI guard,
the live e2e validation (create→prompt→terminate against api.anthropic.com
with the GCP anthropic-api-key) and `claude --plugin-dir` validation, and the
called-out follow-ups (runtime:"managed" unification, deployed HTTP ruflo MCP
server for the cloud-hosted-ruflo combo, task_assign/federation dispatch to a
Managed Agent session, cost-tracking + orphaned-session GC, multiagent/outcomes).
Tracking issue: #1931.

Co-Authored-By: RuFlo <ruv@ruv.net>

* test(cli): no-network guard for managed_agent_* tools (ADR-115)

__tests__/managed-agent-tools.test.ts (10 cases): the 6 managed_agent_*
tools are registered; each has an object inputSchema + a ≥80-char,
ADR-112-ish, unique description that names the wasm_agent_*/native fallback;
and — crucially — every handler degrades gracefully with no ANTHROPIC_API_KEY/
CLAUDE_API_KEY: no throw, structured error mentioning the key + the
wasm_agent_create fallback, no network call (so the CLI/MCP server stays up
offline). Plus managed_agent_prompt's missing-sessionId/message validation.

Co-Authored-By: RuFlo <ruv@ruv.net>

* docs(adr): ADR-115 — add the third runtime (Claude Agent SDK) to the picture

ADR-115 adds the *cloud* runtime (Managed Agents); add a section noting the
natural *third* runtime — the Claude Agent SDK (@anthropic-ai/claude-agent-sdk,
"Claude Code as a library": query()/ClaudeSDKClient, hooks, subagents, MCP,
permission modes, sessions, loads .claude/ config + plugins; runs in-process,
full host trust). ruflo-agent → three runtimes behind one model:

  WASM (rvagent)   local sandbox, no host fs/net   untrusted/portable/offline
  SDK  (future)    your process, your filesystem    a real Claude agent loop on the local repo — the in-process `claude -p`
  Managed          Anthropic cloud container        long-running/async, no local infra

The SDK runtime's killer combo: `mcpServers: { ruflo: { command: "npx", args: ["ruflo","mcp","start"] } }` — a *local* stdio MCP server, so the agent gets ruflo's 314 tools with zero deployment (vs Managed Agents needing a reachable URL). Anthropic positions SDK→Managed as prototype→production. Costs: heavy dep (bundles a Claude Code binary), full host trust (no sandbox — least isolated), needs an API key. Implementing it = a follow-up, likely ADR-116 (tracked in #1931).

Co-Authored-By: RuFlo <ruv@ruv.net>

---------

Co-authored-by: Reuven <cohen@ruv-mac-mini.local>
2026-05-12 10:24:03 -04:00

93 lines
3.8 KiB
TypeScript

/**
* Guard for ADR-115 — the managed_agent_* MCP tools (Claude Managed Agents
* cloud runtime, in the `ruflo-agent` plugin). No-network: every handler must
* short-circuit with a structured "needs ANTHROPIC_API_KEY → use wasm_agent_*"
* error when no key is set, so the CLI/MCP server stays up offline.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { managedAgentTools } from '../src/mcp-tools/managed-agent-tools.js';
const TOOL_NAMES = [
'managed_agent_create',
'managed_agent_prompt',
'managed_agent_status',
'managed_agent_events',
'managed_agent_list',
'managed_agent_terminate',
] as const;
const tool = (name: string) => {
const t = managedAgentTools.find(t => t.name === name);
if (!t) throw new Error(`managed-agent tool not registered: ${name}`);
return t;
};
describe('ADR-115 — managed_agent_* MCP tools', () => {
let prevAnth: string | undefined;
let prevClaude: string | undefined;
beforeEach(() => {
prevAnth = process.env.ANTHROPIC_API_KEY;
prevClaude = process.env.CLAUDE_API_KEY;
delete process.env.ANTHROPIC_API_KEY;
delete process.env.CLAUDE_API_KEY;
});
afterEach(() => {
if (prevAnth === undefined) delete process.env.ANTHROPIC_API_KEY; else process.env.ANTHROPIC_API_KEY = prevAnth;
if (prevClaude === undefined) delete process.env.CLAUDE_API_KEY; else process.env.CLAUDE_API_KEY = prevClaude;
});
it('registers exactly the six managed_agent_* tools', () => {
const names = managedAgentTools.map(t => t.name).sort();
expect(names).toEqual([...TOOL_NAMES].sort());
});
it('every tool has an inputSchema (object) and an ADR-112-ish description that mentions the wasm_agent_* / native fallback', () => {
for (const t of managedAgentTools) {
expect(t.inputSchema?.type).toBe('object');
expect(typeof t.description).toBe('string');
expect(t.description.length).toBeGreaterThanOrEqual(80);
// each description should orient the caller toward wasm_agent_* / native when that's the right call
expect(t.description).toMatch(/wasm_agent_|native/);
expect(t.category).toBe('agent');
}
});
it('descriptions are unique (no copy-paste)', () => {
const descs = managedAgentTools.map(t => t.description);
expect(new Set(descs).size).toBe(descs.length);
});
for (const name of TOOL_NAMES) {
it(`${name} degrades gracefully with no API key (no throw, structured error, no network)`, async () => {
// minimal valid-ish input per tool's required fields
const input: Record<string, unknown> =
name === 'managed_agent_prompt' ? { sessionId: 's', message: 'm' }
: name === 'managed_agent_create' ? {}
: name === 'managed_agent_list' ? {}
: { sessionId: 's' };
const res = (await (tool(name).handler as any)(input)) as Record<string, unknown>;
expect(res).toBeTruthy();
expect(String(res.error ?? '')).toMatch(/ANTHROPIC_API_KEY/);
expect(String(res.error ?? '')).toMatch(/wasm_agent_create/);
// tools that report success should report it false
if ('success' in res) expect(res.success).toBe(false);
// list-shaped tools still return an empty list
if (name === 'managed_agent_list') {
expect(res.sessions).toEqual([]);
expect(res.total).toBe(0);
}
});
}
it('managed_agent_prompt rejects missing sessionId/message (with a key set, but no network call made)', async () => {
process.env.ANTHROPIC_API_KEY = 'sk-ant-test-not-real';
const noSession = await (tool('managed_agent_prompt').handler as any)({ message: 'hi' });
expect(noSession.success).toBe(false);
expect(String(noSession.error)).toMatch(/sessionId/);
const noMessage = await (tool('managed_agent_prompt').handler as any)({ sessionId: 's' });
expect(noMessage.success).toBe(false);
expect(String(noMessage.error)).toMatch(/message/);
});
});