Files
ruvnet__ruflo/scripts/smoke-wasm-gallery-crud.mjs
rUv 47a7825b0e feat(rvagent): #ADR-129 — full rvagent integration (4 phases) (#2123)
* docs(adr): ADR-129 — rvagent full integration (4 phases, post-3.7.0 baseline)

Proposes a 4-phase integration of @ruvector/rvagent-wasm@0.1.0 to make
WasmAgents first-class participants in the ruflo swarm.

## Gaps found in current code (verified by deep-researcher)

- agent-wasm.ts:165-196 — echo-stub bypass (JsModelProvider never
  constructed; verified by `grep -rn 'new JsModelProvider'` returns 0
  hits). WasmAgent.prompt() either echoes or hits a default — not
  routed through the v3 provider system from #2042.
- agent-wasm.ts:420-429 — buildRvfFromTemplate silently drops
  template.mcp_tools, so wasm_gallery_create can't surface MCP tools
  to the agent.
- 10 of 13 gallery methods unexposed (loadRvf, configure, getCategories,
  listByCategory, addCustom, removeCustom, importCustom, exportCustom,
  getActive, getConfig).
- 6 agent introspection methods unexposed (get_state, get_todos,
  get_tools, turn_count, is_stopped, reset).
- No bridge from any of the 35 ruflo plugins to the WASM agent runtime.

## 4 phases

| Phase | What | Smoke |
|---|---|---|
| 1 | JsModelProvider → callAnthropicMessages bridge | smoke-wasm-provider-bridge.mjs |
| 2 | wasm_agent_compose + addMcpTools bridge (314 tools) | smoke-wasm-rvf-compose.mjs |
| 3 | 16 new MCP tools (gallery CRUD + introspection) | smoke-wasm-gallery-crud.mjs |
| 4 | Plugin bridge contract | smoke-wasm-plugin-bridge.mjs |

## Top 3 risks

1. Billing expansion — Phase 1 turns wasm_agent_prompt into billable
   LLM calls (was echo).
2. addMcpTools over-permissioning — Phase 2 default must be explicit
   allowlist, not full dump (314 tools include destructive ones like
   memory_delete, federation_*).
3. importCustom prompt-injection — Phase 3 deserializes user JSON in
   the WASM runtime; AIDefence gate required.

No phase needs a separate ADR. Anchored in #2042 (provider routing),
ADR-115 (rvagent rename), ADR-127 (smoke pattern).

Co-Authored-By: RuFlo <ruflo-bot@users.noreply.github.com>

* feat(rvagent): #ADR-129 P1 — JsModelProvider routes WasmAgent through v3 provider system

Wire set_model_provider() at agent-creation time so the WASM runtime's
internal conversation loop dispatches through callAnthropicMessages
(Anthropic / OpenRouter / Ollama, same routing as agent_execute #2042).
Echo-stub fallback preserved for keyless environments.  Smoke +
CI job added: smoke-wasm-provider-bridge.mjs / wasm-provider-bridge-smoke.

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

* feat(rvagent): #ADR-129 P2 — wasm_agent_compose + addMcpTools bridge (314-tool unlock with allowlist gate)

Fix silent drop of template.mcp_tools in buildRvfFromTemplate; extend
buildRvfContainer to accept mcpTools param → builder.addMcpTools().
Add wasm_agent_compose MCP tool with explicit allowlist, destructive-tool
gate (mcpToolsAllowDestructive required for *_delete/*_shutdown/federation_*),
and includePlugins P4 wire.  Smoke + CI job: smoke-wasm-rvf-compose.mjs.

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

* feat(rvagent): #ADR-129 P3 — 16 new MCP tools (10 gallery CRUD + 6 introspection) with AIDefence gate on import

Add wasm_gallery_load_rvf, configure, categories, list_by_category,
add_custom, remove_custom, import (HIGH_RISK + AIDefence gate), export,
active, config.  Add wasm_agent_state, todos, tools, turn_count,
is_stopped, reset.  Export 10 new functions from agent-wasm.ts.
Smoke + CI job: smoke-wasm-gallery-crud.mjs.

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

* feat(rvagent): #ADR-129 P4 — plugin manifest "rvagent" field + includePlugins in compose

Add loadPluginManifest() + extractPluginSkills() to wasm-agent-tools.ts.
wasm_agent_compose gains includePlugins?: string[] — reads each plugin's
.claude-plugin/plugin.json, parses rvagent.exposeSkillsAsTools, and adds
matching skills to the RVF container.  Unknown plugin name → warning,
not error.  Smoke + CI job: smoke-wasm-plugin-bridge.mjs.

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

* fix(ci): #2120 — memory smoke uses pnpm (workspace: protocol unsupported by npm)

The memory-stats-legacy-db-smoke job hit `EUNSUPPORTEDPROTOCOL:
Unsupported URL Type "workspace:"` in CI because `cd v3/@claude-flow/cli
&& npm install` ran against a package.json with workspace:* deps that
only pnpm understands. Same pattern as the agent-execute smoke fix.

Switch the job to pnpm/action-setup + `pnpm install` + `pnpm --filter
@claude-flow/cli build`. The smoke logic is unchanged.

Co-Authored-By: RuFlo <ruflo-bot@users.noreply.github.com>

* fix(rvagent): #ADR-129 — pad 16 new wasm tool descriptions to ADR-112 spec (>=80 chars + Use-when guidance)

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

* feat(rvagent): #ADR-129 — benchmark suite (provider routing, compose, gallery CRUD, plugin enum)

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

* feat(witness): #ADR-103 — register ADR-129 P1-P4 fixes in witness manifest

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

* fix(ci): #2120 — pnpm install must run from v3/ (workspace root)

My prior fix (2d2091fe7) ran `pnpm install` at repo root, but the
pnpm-workspace.yaml lives in v3/, not at the root. Result: no
node_modules under v3/@claude-flow/cli, tsc errored on missing
@types/node, build never produced dist.

Mirror the working pattern used elsewhere in this workflow:
`working-directory: v3` + `pnpm install` + `pnpm --filter
@claude-flow/cli build`. Keep the root `npm install` for sql.js
which the smoke fixture imports from the repo root.

Co-Authored-By: RuFlo <ruflo-bot@users.noreply.github.com>

* fix(ci): #2120 — recursive build (CLI tsc depends on swarm dist)

Previous fix (cec3cef48) installed correctly from v3/ but the CLI's tsc
emits TS6305 because it has project references to @claude-flow/swarm
and its dist hasn't been produced yet. `pnpm --filter @claude-flow/cli
build` only builds that one package and skips deps.

Switch to `pnpm --recursive --no-bail run build || true` — the proven
pattern used elsewhere in this workflow — then assert the specific dist
file the smoke imports (`dist/src/memory/memory-initializer.js`).

Co-Authored-By: RuFlo <ruflo-bot@users.noreply.github.com>

* fix(memory): #2120 — getBridge honors CLAUDE_FLOW_DISABLE_BRIDGE=1

The #2120 smoke sets `CLAUDE_FLOW_DISABLE_BRIDGE=1` to force the raw
sql.js fallback path of `listEntries`, because the ensureSchemaColumns
backfill (NULL status → 'active') only runs in that fallback —
bridgeListEntries reads via a separate better-sqlite3 handle and skips
the migration.

`getBridge()` ignored that env var, so on CI Linux (where better-sqlite3
builds successfully) the bridge took over `listEntries`, the backfill
never ran, and the smoke's step-4 verification (reading the file with
plain sql.js) saw status=NULL on all rows and reported 0 active. The
test passed on macOS only because the bridge happened to fail to
initialize locally.

Honor the env var: when set to "1", short-circuit getBridge() before any
import attempt. Same value the smoke already sets — no other call site
relies on this flag, so the change is non-breaking.

Co-Authored-By: RuFlo <ruflo-bot@users.noreply.github.com>

---------

Co-authored-by: RuFlo <ruflo-bot@users.noreply.github.com>
2026-05-24 14:32:43 -04:00

141 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Regression guard for ADR-129 Phase 3 — 16 new MCP tools (10 gallery
* CRUD + 6 agent introspection) with AIDefence gate on wasm_gallery_import.
*
* Pre-P3: only 3 of 13 WasmGallery methods were exposed as MCP tools.
* Agent introspection (get_state, get_todos, reset) had no MCP surface.
*
* P3 adds:
* Gallery (10): load_rvf, configure, categories, list_by_category,
* add_custom, remove_custom, import (AIDefence-gated),
* export, active, config
* Introspect (6): state, todos, tools, turn_count, is_stopped, reset
*
* Static contracts (no build required):
* 1. All 16 new tools registered in wasm-agent-tools.ts
* 2. wasm_gallery_import has HIGH_RISK marker + AIDefence call
* 3. wasm_agent_reset calls resetWasmAgent
* 4. wasm_agent_todos exists
* 5. wasm_agent_state exists
* 6. agent-wasm.ts exports the 9 new gallery functions
* 7. agent-wasm.ts exports resetWasmAgent
*/
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const TOOLS = resolve(__dirname, '../v3/@claude-flow/cli/src/mcp-tools/wasm-agent-tools.ts');
const WASM = resolve(__dirname, '../v3/@claude-flow/cli/src/ruvector/agent-wasm.ts');
function fail(msg) { console.error(`✗ ${msg}`); process.exitCode = 1; }
function pass(msg) { console.log(`✓ ${msg}`); }
const toolsSrc = readFileSync(TOOLS, 'utf8');
const wasmSrc = readFileSync(WASM, 'utf8');
const NEW_GALLERY_TOOLS = [
'wasm_gallery_load_rvf',
'wasm_gallery_configure',
'wasm_gallery_categories',
'wasm_gallery_list_by_category',
'wasm_gallery_add_custom',
'wasm_gallery_remove_custom',
'wasm_gallery_import',
'wasm_gallery_export',
'wasm_gallery_active',
'wasm_gallery_config',
];
const NEW_INTROSPECT_TOOLS = [
'wasm_agent_state',
'wasm_agent_todos',
'wasm_agent_tools',
'wasm_agent_turn_count',
'wasm_agent_is_stopped',
'wasm_agent_reset',
];
// 1. All gallery tools registered
let allGalleryOk = true;
for (const t of NEW_GALLERY_TOOLS) {
const re = new RegExp(`name:\\s*['"]${t}['"]`);
if (!re.test(toolsSrc)) {
fail(`Gallery tool not registered: ${t}`);
allGalleryOk = false;
}
}
if (allGalleryOk) pass(`All 10 gallery CRUD tools registered (${NEW_GALLERY_TOOLS.join(', ')})`);
// 2. All introspection tools registered
let allIntrospectOk = true;
for (const t of NEW_INTROSPECT_TOOLS) {
const re = new RegExp(`name:\\s*['"]${t}['"]`);
if (!re.test(toolsSrc)) {
fail(`Introspection tool not registered: ${t}`);
allIntrospectOk = false;
}
}
if (allIntrospectOk) pass(`All 6 introspection tools registered (${NEW_INTROSPECT_TOOLS.join(', ')})`);
// 3. wasm_gallery_import has HIGH_RISK marker + AIDefence call
// Search the section between wasm_gallery_import tool name and wasm_gallery_export
const importStart = toolsSrc.indexOf("'wasm_gallery_import'");
const importEnd = toolsSrc.indexOf("'wasm_gallery_export'");
if (importStart < 0) {
fail('wasm_gallery_import tool not found');
} else {
const importSection = toolsSrc.slice(importStart, importEnd > importStart ? importEnd : importStart + 2000);
if (!/HIGH.?RISK|HIGH_RISK/.test(importSection)) {
fail('wasm_gallery_import missing HIGH_RISK marker (ADR-129 P3 security requirement)');
} else {
pass('wasm_gallery_import has HIGH_RISK marker');
}
if (!/aidefence|AIDefence|createAIDefence/.test(importSection)) {
fail('wasm_gallery_import missing AIDefence gate call (ADR-129 P3 security requirement)');
} else {
pass('wasm_gallery_import has AIDefence gate');
}
}
// 4. agent-wasm.ts exports new gallery functions
const NEW_WASM_EXPORTS = [
'galleryLoadRvf',
'galleryConfigure',
'galleryListByCategory',
'galleryAddCustom',
'galleryRemoveCustom',
'galleryImportCustom',
'galleryExportCustom',
'galleryGetActive',
'galleryGetConfig',
'resetWasmAgent',
];
let allExportsOk = true;
for (const fn of NEW_WASM_EXPORTS) {
if (!new RegExp(`export (?:async )?function ${fn}`).test(wasmSrc)) {
fail(`agent-wasm.ts missing export: ${fn}`);
allExportsOk = false;
}
}
if (allExportsOk) pass(`agent-wasm.ts exports all 10 new functions`);
// 5. resetWasmAgent calls agent.reset()
const resetFn = wasmSrc.match(/export function resetWasmAgent[\s\S]*?\n\}/);
if (!resetFn) {
fail('resetWasmAgent not found in agent-wasm.ts');
} else if (!/\.reset\(\)/.test(resetFn[0])) {
fail('resetWasmAgent does not call agent.reset()');
} else {
pass('resetWasmAgent calls agent.reset()');
}
if (process.exitCode) {
console.error('\nADR-129 P3 gallery CRUD smoke FAILED');
} else {
console.log('\nADR-129 P3 gallery CRUD smoke PASS');
}