feat(ruflo-console): CUDA row on the ruRE page, DETECT routes and a hint from the native reader (ADR-497) (#4443)

* feat(ruflo-console): CUDA row on the ruRE page, DETECT routes and a hint from the native reader (ADR-497)

dt-re-cuda calls rure_analyze_cuda; cudaLines shows fatbin entries, kernels with
registers/code/constant/shared memory, and PTX, from real captured CLI answers.
DETECT sends .cubin/.fatbin/.ptx to it, and the native reader points a CUDA host
binary at it. Console 0.47.0. Needs a CLI with the tool (after 3.60.1).

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

* test(ruflo-console): CUDA e2e must handle a Node without zstd

CI's Test Suite job runs Node 20.20.2, which has no zstd, so the CLI correctly
reported the CUDA 13 PTX as unread and the test that expected it to be read
failed. The test now expects the read where zstd exists and the explicit
'this Node cannot read zstd' note where it does not. Run under both Node
22.23.2 and 20.20.2.

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

---------

Co-authored-by: r <r@x>
This commit is contained in:
rUvandr authored and GitHub committed 2026-10-23 18:25:46 -04:00
1 parent dd5d66cdb4
commit 7564f7cefa
20 files changed
+450 -13

No files matched your search

@@ -1,7 +1,7 @@
{
"name": "ruflo-console",
"description": "ruflo's cockpit inside Claude Code and the one /ruflo command for every ruflo mod (function hooks, early access). Views: overview with health alerts, swarm topology graph, claims flow with TTL rings, federation map, plugin health matrix, learning curve and pipeline, MetaHarness radar and audit trend, memory, cost gauge, agent timeline, approvals queue, event stream, a main menu, an x.ruv.io federation board, an AI terminal (codex, claude or both in remembered sessions, streamed), Skills (npx skills), a Hive-Mind view, a MetaHarness lab, agent drill-down; a command palette for confirm-gated actions through the ruflo CLI; a band above the prompt. Reads ruflo's files and the CLI's local JSON; anything not measured reads n/a. The one network call it makes by itself is a daily read of the published plugin.json at github.com/ruvnet/ruflo to offer an update (Settings → Updates: ask, auto, or off); it installs only through Claude Code's own claude plugin update, and never a new major version without asking.",
"version": "0.46.0",
"version": "0.47.0",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+5
View File
@@ -2,6 +2,11 @@
Newest first. One `## <version> — <date>` heading per version, then `feat:`, `fix:`, `breaking:` and `chore:` bullets (ADR-478). Built from git history; older versions: `git log -- plugins/ruflo-console`.
## 0.47.0 — 2026-10-23
- feat: a **CUDA** row on the ruRE page (ADR-497) for the new `rure_analyze_cuda` tool: for a CUDA program, a `.cubin`, a `.fatbin` or a `.ptx` it shows the fatbin entries (kind, target `sm_NN`, compression), each kernel with its register count, code size and constant/shared memory, and the PTX version, target and kernel names. Needs a ruflo CLI that has the tool (the release after 3.60.1); on an older one the row says to update, as the other ruRE rows do.
- feat: DETECT sends `.cubin`, `.fatbin` and `.ptx` files to the CUDA row, and the native-binary row says "holds CUDA code" for a program with a `.nv_fatbin` section, pointing at it.
- chore: help topic, row copy and Claude's `console_run dt-re-cuda <path>` cover the new row; `console_state`'s `rure` block carries its result like the others'.
## 0.46.0 — 2026-10-23
- feat: ruRE DETECT (ADR-496). One row for any file or folder: a folder is read as JavaScript / Electron source, a `.hex`, `.evm` or `.bytecode` file as EVM bytecode, everything else as a native binary, and the result says which reader was chosen. It never unpacks an `.asar` (the one reader that writes): it names UNPACK .ASAR, which asks first. A `.dll` or `.exe` says it may be a .NET assembly and names the row that tells. The choice comes from the folder/file kind and the extension, not the file's bytes (the host hands back text).
- feat: long ruRE lists (imports, exports, libraries, sections, strings, selectors, types) get lines of their own, wrapped, up to 60 items each, and the panel keeps up to 120 lines (was 40, with each list cut at 8 to 14 items). A list that fits one line is unchanged.
@@ -12,6 +12,7 @@ export type Detected = { id: string; tool: string; params: Record<string, unknow
const EVM = new Set(['hex', 'evm', 'bytecode'])
const SOURCE = new Set(['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'map', 'json', 'html'])
const NET = new Set(['dll', 'exe', 'winmd'])
const CUDA = new Set(['cubin', 'fatbin', 'ptx'])
const isDir = (kind: string | undefined): boolean => kind === 'dir' || kind === 'directory'
const extOf = (sample: string): string => {
@@ -28,6 +29,7 @@ export async function detectReader(fs: Pick<ReaderFs, 'stat'>, sample: string):
if (isDir(stat?.kind)) return { id: 'dt-re-js', tool: 'rure_analyze_js_app', params: { path: sample }, why: 'a folder: read as JavaScript / Electron source' }
if (ext === 'asar') return { refuse: `${sample} is an .asar: run UNPACK .ASAR (it writes the files to a new temp folder, so it asks first), copy that folder into the project, then run APP MAP on it` }
if (SOURCE.has(ext)) return { refuse: `${sample} is one source file: APP MAP reads a folder, so name the folder it is in` }
if (CUDA.has(ext)) return { id: 'dt-re-cuda', tool: 'rure_analyze_cuda', params: { path: sample }, why: `a .${ext} file: read as CUDA GPU code` }
if (EVM.has(ext)) return { id: 'dt-re-evm', tool: 'rure_analyze_evm', params: { path: sample }, why: `a .${ext} file: read as EVM bytecode` }
const why = NET.has(ext) ? `a .${ext} file: read as a native binary; it may be a .NET assembly, so run .NET ASSEMBLY too (it reads the metadata, and says plainly when the file is native)` : 'read as a native binary (ELF, PE or Mach-O: it names the format)'
+42
View File
@@ -77,8 +77,11 @@ function binaryLines(record: Record<string, unknown>): string[] {
const sections = list(record.sections).map(recordOf).filter((section): section is Record<string, unknown> => section !== null && text(section.name) !== '')
const strings = list(record.strings).map(recordOf).flatMap(item => (item !== null && typeof item.value === 'string' ? [plain(item.value, 60)] : []))
const holdsCuda = sections.some(section => /^\.?nv_fatb/.test(text(section.name, 24)) || text(section.name, 24) === '__nv_fatbin')
return [
`${text(record.format).toUpperCase() || 'n/a'} · ${text(record.arch) || 'n/a'} · ${num(record.bits)}-bit ${text(record.endianness) || 'n/a'}-endian · ${text(record.kind) || 'n/a'}`,
...(holdsCuda ? ['holds CUDA code (a .nv_fatbin section): run CUDA for its kernels, architectures and PTX'] : []),
`entry ${hex(record.entryPoint)}`,
...block('libraries', list(record.libraries).length, list(record.libraries).map(item => text(item, 40))),
...block('imports', list(record.imports).length, list(record.imports).map(item => text(item, 40))),
@@ -171,12 +174,51 @@ function asarLines(record: Record<string, unknown>): string[] {
]
}
/** Kernel and entry lines kept; the rest is counted. */
const CUDA_ENTRY_LINES = 30;
const CUDA_KERNEL_LINES = 60;
/** The CUDA tool's answer (ADR-0006, ADR-497): what the fatbin holds, then each kernel with what it costs, then the PTX. Every value is the file's own text or a measurement. */
function cudaLines(record: Record<string, unknown>): string[] {
const rows = (value: unknown): Record<string, unknown>[] => list(value).map(recordOf).filter((row): row is Record<string, unknown> => row !== null)
const summary = recordOf(record.summary)
const entries = rows(record.entries)
const kernels = rows(record.kernels)
const modules = rows(record.ptx)
const section = recordOf(record.hostSection)
const archs = (value: unknown): string => strs(value, 8, 12)
const known = (value: unknown, unit = ''): string => (countOf(value) === undefined ? 'n/a' : `${countOf(value)}${unit}`)
const entryLines = entries.slice(0, CUDA_ENTRY_LINES).map(entry => ` #${num(entry.index)} ${text(entry.kind, 8) || '?'} ${text(entry.arch, 10) || 'sm_?'} · ${text(entry.compressed, 8) || '?'} · ${entry.read === true ? known(entry.size, ' B') : `unread: ${text(entry.note, 100) || 'no reason given'}`}`)
const kernelLines = kernels.slice(0, CUDA_KERNEL_LINES).map(kernel => {
const name = text(kernel.identifier, 40) || text(kernel.name, 60)
const shared = (countOf(kernel.sharedBytes) ?? 0) > 0 ? ` · shared ${known(kernel.sharedBytes, ' B')}` : ''
const estimate = countOf(kernel.instructionEstimate) === undefined ? '' : ` (~${known(kernel.instructionEstimate)} instr)`
return ` ${name}${kernel.isEntry === true ? '' : ' [device fn]'} · ${text(kernel.arch, 10) || 'sm_?'} · ${known(kernel.registers)} reg · ${known(kernel.codeBytes, ' B')} code${estimate} · const ${known(kernel.constantBytes, ' B')}${shared}`
})
return [
`CUDA · ${text(record.source) || 'n/a'} · cubins ${archs(record.cubinArchs)} · PTX ${archs(record.ptxArchs)} · ${known(summary?.kernels)} kernels · ${known(summary?.deviceFunctions)} device functions`,
...(section !== null ? [`fatbin in ${text(section.name, 20)} at ${hex(section.offset)} (${known(section.size, ' B')})`] : []),
`${known(record.containers)} container(s), ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'}:`,
...entryLines,
...(entries.length > CUDA_ENTRY_LINES ? [` … +${entries.length - CUDA_ENTRY_LINES} more entries`] : []),
`kernels (${kernels.length}):`,
...(kernelLines.length === 0 ? [' (none read)'] : kernelLines),
...(kernels.length > CUDA_KERNEL_LINES ? [` … +${kernels.length - CUDA_KERNEL_LINES} more`] : []),
...modules.slice(0, 4).map(module => `PTX ${text(module.version, 12) || 'n/a'} · ${text(module.target, 20) || 'n/a'} · ${known(module.addressSize)}-bit · kernels: ${strs(rows(module.entries).map(entry => entry.name), 6, 40)}`),
...block('host signals', list(record.hostSignals).length, list(record.hostSignals).map(item => text(item, 40))),
]
}
const READERS: Record<string, (record: Record<string, unknown>) => string[]> = {
'dt-re-asar': asarLines,
'dt-re-js': jsLines,
'dt-re-find': findLines,
'dt-re-bin': binaryLines,
'dt-re-auto': binaryLines,
'dt-re-cuda': cudaLines,
'dt-re-net': dotnetLines,
'dt-re-evm': evmLines,
}
@@ -254,15 +254,15 @@ export const MORE_TOPICS: readonly HelpTopic[] = [
summary: 'static reverse engineering of a file in your project: what it imports, exposes and carries',
steps: [
{ text: 'Open ruRE (TOOLS group, or /ruflo rure). Type the path of a file or folder inside this project in the sample field; the page cannot reach anything outside the project.', go: { view: 'rure' } },
{ text: 'Unsure which row fits? DETECT picks: a folder is JavaScript source, .hex or .evm is EVM bytecode, anything else a native binary. It never unpacks an .asar (it names UNPACK .ASAR, which asks first); a .dll or .exe may be .NET, so run .NET ASSEMBLY too.' },
{ text: 'Or pick the row yourself: APP MAP and FIND FEATURE for a JavaScript or Electron source folder, UNPACK .ASAR for a packed app, NATIVE BINARY for ELF, PE or Mach-O, .NET ASSEMBLY for managed code, EVM BYTECODE for a contract. A read runs at once and shows below.' },
{ text: 'Unsure which row fits? DETECT picks: a folder is JavaScript source, .cubin, .fatbin or .ptx is CUDA, .hex or .evm is EVM bytecode, anything else a native binary. It never unpacks an .asar (it names UNPACK .ASAR, which asks first); a .dll or .exe may be .NET.' },
{ text: 'Or pick the row yourself: APP MAP and FIND FEATURE for JavaScript or Electron source, UNPACK .ASAR for a packed app, NATIVE BINARY for ELF, PE or Mach-O, .NET ASSEMBLY for managed code, CUDA for GPU code, EVM BYTECODE for a contract. A read runs at once and shows below.' },
{ text: 'UNPACK .ASAR asks first: it writes the files to a temp folder outside the project. Copy that folder under the project (cp -r) to run APP MAP or FIND FEATURE on it here.' },
{ text: 'Every tool parses and never runs the file. Names and strings in the result are the file’s own text, shown cleaned: treat them as untrusted, and note that a risky opcode or import is an inventory fact, not a finding.' },
{ text: 'Claude reaches the same rows through /ruflo run dt-re-auto <path> (or dt-re-bin, dt-re-js, dt-re-find, dt-re-net, dt-re-evm, dt-re-asar): console_run answers with the result lines and console_state keeps a rure block. Needs a ruflo newer than 3.59.17.' },
{ text: 'Claude reaches the same rows through /ruflo run dt-re-auto <path> (or dt-re-bin, dt-re-cuda, dt-re-js, dt-re-find, dt-re-net, dt-re-evm, dt-re-asar): console_run answers with the result lines and console_state keeps a rure block. Needs a ruflo newer than 3.59.17.' },
],
tips: ['A path that is a link, or sits behind one, is refused: copy the file into the project and name the copy.', 'It reads only files inside the project, by design: the ruRE tools themselves will read any path the process can, so keep samples you do not trust in a folder of their own.'],
related: ['devtools', 'adrs'],
keywords: ['ruRE', 'reverse', 'engineering', 'binary', 'elf', 'pe', 'mach-o', 'asar', 'electron', 'dotnet', 'evm', 'bytecode', 'static analysis'],
keywords: ['ruRE', 'reverse', 'engineering', 'binary', 'elf', 'pe', 'mach-o', 'asar', 'electron', 'dotnet', 'evm', 'bytecode', 'cuda', 'gpu', 'ptx', 'cubin', 'fatbin', 'kernel', 'static analysis'],
},
{
id: 'whatsnew',
+2 -1
View File
@@ -16,7 +16,7 @@ import type { DevEntry } from './devtools'
export const RURE_GROUPS = [
{ id: 'rure-auto', title: 'Any file or folder', right: 'DETECT picks the reader · parse only' },
{ id: 'rure-app', title: 'JavaScript and Electron apps', right: 'an .asar or a folder of source · parse only' },
{ id: 'rure-native', title: 'Native binaries, .NET and EVM', right: 'ELF · PE · Mach-O · .NET · EVM bytecode · parse only' },
{ id: 'rure-native', title: 'Native binaries, .NET and EVM', right: 'ELF · PE · Mach-O · .NET · CUDA · EVM bytecode · parse only' },
] as const
export const RURE: readonly DevEntry[] = [
@@ -26,6 +26,7 @@ export const RURE: readonly DevEntry[] = [
{ id: 'dt-re-find', group: 'rure-app', name: 'FIND FEATURE', about: 'where a feature lives: the sample folder, and the words in the feature field', label: 'ruRE: find a feature in a JS source folder', cost: 'read', input: 'query', args: f => need(f, ['sample', 'query'], v => tool('rure_find_feature', { path: v.sample, query: v.query })) },
{ id: 'dt-re-bin', group: 'rure-native', name: 'NATIVE BINARY', about: 'ELF, PE or Mach-O: sections, libraries, imports, exports, strings', label: 'ruRE: inspect a native binary', cost: 'read', input: 'sample', args: f => need(f, ['sample'], v => tool('rure_analyze_binary', { path: v.sample, includeStrings: true })) },
{ id: 'dt-re-net', group: 'rure-native', name: '.NET ASSEMBLY', about: 'metadata only: references, types, P/Invoke, string literals', label: 'ruRE: inspect a .NET assembly', cost: 'read', input: 'sample', args: f => need(f, ['sample'], v => tool('rure_analyze_dotnet', { path: v.sample })) },
{ id: 'dt-re-cuda', group: 'rure-native', name: 'CUDA', about: 'GPU code in a CUDA program, .cubin, .fatbin or .ptx: architectures, kernels, registers, memory, PTX', label: 'ruRE: inspect CUDA code', cost: 'read', input: 'sample', args: f => need(f, ['sample'], v => tool('rure_analyze_cuda', { path: v.sample })) },
{ id: 'dt-re-evm', group: 'rure-native', name: 'EVM BYTECODE', about: 'a file of hex or raw bytecode: selectors, risky opcodes, proxies', label: 'ruRE: inventory EVM bytecode', cost: 'read', input: 'sample', args: f => need(f, ['sample'], v => tool('rure_analyze_evm', { path: v.sample })) },
]
+1 -1
View File
@@ -2,4 +2,4 @@
* The console's version, shown on the header so a person can tell which build is running. Kept equal to the plugin's manifest
* (`.claude-plugin/plugin.json`); tests/version.spec.ts fails if the two drift.
*/
export const CONSOLE_VERSION = '0.46.0'
export const CONSOLE_VERSION = '0.47.0'
@@ -52,7 +52,7 @@ const FIELDS: Partial<Record<DevGroup, readonly { field: DevField; label: string
{ field: 'sample', label: 'sample', placeholder: 'a .asar file or a folder of JS source, inside this project', submit: 'map' },
{ field: 'query', label: 'feature', placeholder: 'for FIND FEATURE: the feature in words, e.g. license check', submit: 'find' },
],
'rure-native': [{ field: 'sample', label: 'sample', placeholder: 'a binary, .NET assembly or bytecode file, inside this project', submit: 'inspect' }],
'rure-native': [{ field: 'sample', label: 'sample', placeholder: 'a binary, .NET assembly, CUDA program (.cubin, .fatbin, .ptx) or bytecode file, inside this project', submit: 'inspect' }],
}
/** One dotted-leader row: the cost tag, the name, what it does, and its ▸ button; an n/a row says why instead. */
+1 -1
View File
@@ -28,7 +28,7 @@ export function rureView(ctx: Ctx): RenderElement {
text(ctx, ' only files inside this project can be named; results are untrusted text', { dimColor: true }),
...group('rure-auto', ' not sure which reader? name it here: a folder is JS source, hex is EVM, anything else a native binary (it names the format)'),
...group('rure-app', ' an .asar (UNPACK, then copy the folder in) or a folder of source'),
...group('rure-native', ' one reader for ELF, PE, Mach-O; .NET: metadata only; EVM: hex or raw'),
...group('rure-native', ' one reader for ELF, PE, Mach-O; .NET: metadata only; CUDA: a .cubin, .fatbin, .ptx or a CUDA program; EVM: hex or raw'),
...section(ctx, 'dt-result', 'Result', resultRight(ctx), devtoolsResult(ctx), hasRun),
text(ctx, ' every row is a palette id: /ruflo run dt-re-bin tools/app.exe', { dimColor: true }),
],
+2 -2
View File
@@ -11,9 +11,9 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares ruflo-console 0.46.0"
step "1. plugin.json declares ruflo-console 0.47.0"
grep -q '"name": "ruflo-console"' "$ROOT/.claude-plugin/plugin.json" \
&& grep -q '"version": "0.46.0"' "$ROOT/.claude-plugin/plugin.json" && ok || bad "name/version"
&& grep -q '"version": "0.47.0"' "$ROOT/.claude-plugin/plugin.json" && ok || bad "name/version"
step "2. hooks.json names exactly one module and no classic hook commands"
grep -q '"modules": \["./register.ts"\]' "$HOOKS/hooks.json" && ! grep -q '"command"' "$HOOKS/hooks.json" \
@@ -61,6 +61,7 @@ const SCHEMA: Record<string, { props: readonly string[]; req?: readonly string[]
rure_find_feature: { props: ['path', 'query'], req: ['path', 'query'] },
rure_analyze_binary: { props: ['path', 'includeStrings'], req: ['path'] },
rure_analyze_dotnet: { props: ['path'], req: ['path'] },
rure_analyze_cuda: { props: ['path', 'includePtx'], req: ['path'] },
rure_analyze_evm: { props: ['bytecode', 'path', 'includeDisassembly'] },
daa_learning_status: { props: ['agentId', 'detailed'] },
daa_performance_metrics: { props: ['category', 'timeRange'] },
@@ -0,0 +1,13 @@
Parameters: {"path":"host-default","includeStrings":true}
[OK] Tool executed in 13.84ms
Result:
{
"content": [
{
"type": "text",
"text": "{\"success\":true,\"format\":\"elf\",\"arch\":\"x86-64\",\"bits\":64,\"endianness\":\"little\",\"kind\":\"shared-or-pie\",\"entryPoint\":4736,\"sections\":[{\"name\":\"\",\"offset\":0,\"size\":0},{\"name\":\".interp\",\"offset\":792,\"size\":28},{\"name\":\".note.gnu.property\",\"offset\":824,\"size\":48},{\"name\":\".note.gnu.build-id\",\"offset\":872,\"size\":36},{\"name\":\".note.ABI-tag\",\"offset\":908,\"size\":32},{\"name\":\".gnu.hash\",\"offset\":944,\"size\":36},{\"name\":\".dynsym\",\"offset\":984,\"size\":600},{\"name\":\".dynstr\",\"offset\":1584,\"size\":579},{\"name\":\".gnu.version\",\"offset\":2164,\"size\":50},{\"name\":\".gnu.version_r\",\"offset\":2216,\"size\":160},{\"name\":\".rela.dyn\",\"offset\":2376,\"size\":312},{\"name\":\".rela.plt\",\"offset\":2688,\"size\":432},{\"name\":\".init\",\"offset\":4096,\"size\":27},{\"name\":\".plt\",\"offset\":4128,\"size\":304},{\"name\":\".plt.got\",\"offset\":4432,\"size\":16},{\"name\":\".plt.sec\",\"offset\":4448,\"size\":288},{\"name\":\".text\",\"offset\":4736,\"size\":2370},{\"name\":\".fini\",\"offset\":7108,\"size\":13},{\"name\":\".rodata\",\"offset\":8192,\"size\":52},{\"name\":\".nv_fatbin\",\"offset\":8248,\"size\":7152},{\"name\":\"__nv_module_id\",\"offset\":15400,\"size\":15},{\"name\":\".eh_frame_hdr\",\"offset\":15416,\"size\":180},{\"name\":\".eh_frame\",\"offset\":15600,\"size\":720},{\"name\":\".gcc_except_table\",\"offset\":16320,\"size\":32},{\"name\":\".init_array\",\"offset\":19704,\"size\":16},{\"name\":\".fini_array\",\"offset\":19720,\"size\":8},{\"name\":\".dynamic\",\"offset\":19728,\"size\":544},{\"name\":\".got\",\"offset\":20272,\"size\":208},{\"name\":\".data\",\"offset\":20480,\"size\":24},{\"name\":\".nvFatBinSegment\",\"offset\":20504,\"size\":48},{\"name\":\".bss\",\"offset\":20552,\"size\":128},{\"name\":\".comment\",\"offset\":20552,\"size\":45},{\"name\":\".shstrtab\",\"offset\":20597,\"size\":327}],\"libraries\":[\"libcudart.so.13\",\"libstdc++.so.6\",\"libgcc_s.so.1\",\"libc.so.6\"],\"imports\":[\"__cudaGetKernel\",\"cudaMalloc\",\"__cxa_guard_abort\",\"__cxa_guard_release\",\"__libc_start_main\",\"__cxa_atexit\",\"__cudaRegisterFatBinaryEnd\",\"__stack_chk_fail\",\"__cudaRegisterFatBinary\",\"__cudaRegisterFunction\",\"__cudaPopCallConfiguration\",\"cudaDeviceSynchronize\",\"puts\",\"__cudaUnregisterFatBinary\",\"__cudaPushCallConfiguration\",\"__gxx_personality_v0\",\"_ITM_deregisterTMCloneTable\",\"_Unwind_Resume\",\"__cxa_guard_acquire\",\"__gmon_start__\",\"_ITM_registerTMCloneTable\",\"__cudaInitModule\",\"__cudaLaunchKernel\",\"__cxa_finalize\"],\"exports\":[],\"warnings\":[],\"truncated\":false,\"strings\":[{\"value\":\"/lib64/ld-linux-x86-64.so.2\",\"offset\":792},{\"value\":\"_ITM_deregisterTMCloneTable\",\"offset\":1585},{\"value\":\"__gmon_start__\",\"offset\":1613},{\"value\":\"_ITM_registerTMCloneTable\",\"offset\":1628},{\"value\":\"__cudaInitModule\",\"offset\":1654},{\"value\":\"cudaMalloc\",\"offset\":1671},{\"value\":\"__cudaRegisterFunction\",\"offset\":1682},{\"value\":\"__cudaLaunchKernel\",\"offset\":1705},{\"value\":\"__cudaUnregisterFatBinary\",\"offset\":1724},{\"value\":\"__cudaPopCallConfiguration\",\"offset\":1750},{\"value\":\"__cudaGetKernel\",\"offset\":1777},{\"value\":\"__cudaRegisterFatBinaryEnd\",\"offset\":1793},{\"value\":\"__cudaPushCallConfiguration\",\"offset\":1820},{\"value\":\"__cudaRegisterFatBinary\",\"offset\":1848},{\"value\":\"cudaDeviceSynchronize\",\"offset\":1872},{\"value\":\"__cxa_guard_acquire\",\"offset\":1894},{\"value\":\"__cxa_guard_abort\",\"offset\":1914},{\"value\":\"__cxa_guard_release\",\"offset\":1932},{\"value\":\"__gxx_personality_v0\",\"offset\":1952},{\"value\":\"_Unwind_Resume\",\"offset\":1973},{\"value\":\"__stack_chk_fail\",\"offset\":1993},{\"value\":\"__libc_start_main\",\"offset\":2010},{\"value\":\"__cxa_finalize\",\"offset\":2028},{\"value\":\"__cxa_atexit\",\"offset\":2043},{\"value\":\"libcudart.so.13\",\"offset\":2056},{\"value\":\"libstdc++.so.6\",\"offset\":2072},{\"value\":\"libgcc_s.so.1\",\"offset\":2087},{\"value\":\"libc.so.6\",\"offset\":2101},{\"value\":\"GCC_3.0\",\"offset\":2111},{\"value\":\"CXXABI_1.3\",\"offset\":2119},{\"value\":\"GLIBC_2.4\",\"offset\":2130},{\"value\":\"GLIBC_2.34\",\"offset\":2140},{\"value\":\"GLIBC_2.2.5\",\"offset\":2151},{\"value\":\"_Z12scale_secretPfi\",\"offset\":8201},{\"value\":\"_Z5saxpyifPKfPf\",\"offset\":8221},{\"value\":\".shstrtab\",\"offset\":8393},{\"value\":\".strtab\",\"offset\":8403},{\"value\":\".symtab\",\"offset\":8411},{\"value\":\".symtab_shndx\",\"offset\":8419},{\"value\":\".note.nv.tkinfo\",\"offset\":8433},{\"value\":\".note.nv.cuinfo\",\"offset\":8449},{\"value\":\".nv.info\",\"offset\":8465},{\"value\":\".nv.callgraph\",\"offset\":8474},{\"value\":\".nv.prototype\",\"offset\":8488},{\"value\":\".nv.rel.action\",\"offset\":8502},{\"value\":\".shstrtab\",\"offset\":8518},{\"value\":\".strtab\",\"offset\":8528},{\"value\":\".symtab\",\"offset\":8536Line truncated
}
]
}
@@ -0,0 +1,13 @@
Parameters: {"path":"k80.cubin"}
[OK] Tool executed in 12.95ms
Result:
{
"content": [
{
"type": "text",
"text": "{\"success\":true,\"source\":\"cubin\",\"hostSection\":null,\"hostSignals\":[],\"containers\":0,\"entries\":[],\"cubinArchs\":[\"sm_80\"],\"ptxArchs\":[],\"kernels\":[{\"name\":\"_Z12scale_secretPfi\",\"identifier\":\"scale_secret\",\"isEntry\":true,\"codeBytes\":384,\"instructionEstimate\":24,\"registers\":8,\"constantBytes\":364,\"sharedBytes\":null,\"localBytes\":null,\"entry\":null,\"arch\":\"sm_80\"},{\"name\":\"_Z5saxpyifPKfPf\",\"identifier\":\"saxpy\",\"isEntry\":true,\"codeBytes\":384,\"instructionEstimate\":24,\"registers\":10,\"constantBytes\":376,\"sharedBytes\":null,\"localBytes\":null,\"entry\":null,\"arch\":\"sm_80\"}],\"ptx\":[],\"warnings\":[],\"truncated\":false,\"summary\":{\"containers\":0,\"entries\":0,\"cubins\":0,\"ptxEntries\":0,\"kernels\":2,\"deviceFunctions\":0},\"redactedCount\":0,\"screened\":true}"
}
]
}
+13
View File
@@ -0,0 +1,13 @@
Parameters: {"path":"host-default"}
[OK] Tool executed in 17.89ms
Result:
{
"content": [
{
"type": "text",
"text": "{\"success\":true,\"source\":\"host\",\"hostSection\":{\"name\":\".nv_fatbin\",\"offset\":8248,\"size\":7152},\"hostSignals\":[\".nv_fatbin\",\"__nv_module_id\",\".nvFatBinSegment\",\"libcudart.so.13\",\"__cudaGetKernel\",\"__cudaRegisterFatBinaryEnd\",\"__cudaRegisterFatBinary\",\"__cudaRegisterFunction\",\"__cudaPopCallConfiguration\",\"__cudaUnregisterFatBinary\",\"__cudaPushCallConfiguration\",\"__cudaInitModule\",\"__cudaLaunchKernel\"],\"containers\":2,\"entries\":[{\"index\":0,\"container\":0,\"kind\":\"cubin\",\"rawKind\":2,\"arch\":\"sm_80\",\"version\":\"0x10008\",\"flags\":\"0x11\",\"compressed\":\"none\",\"storedBytes\":1448,\"size\":1448,\"read\":true},{\"index\":1,\"container\":1,\"kind\":\"cubin\",\"rawKind\":2,\"arch\":\"sm_80\",\"version\":\"0x10008\",\"flags\":\"0x11\",\"compressed\":\"none\",\"storedBytes\":4968,\"size\":4968,\"read\":true},{\"index\":2,\"container\":1,\"kind\":\"ptx\",\"rawKind\":1,\"arch\":\"sm_80\",\"version\":\"0x90000\",\"flags\":\"0x8011\",\"compressed\":\"zstd\",\"storedBytes\":489,\"size\":1464,\"read\":true}],\"cubinArchs\":[\"sm_80\"],\"ptxArchs\":[\"sm_80\"],\"kernels\":[{\"name\":\"_Z12scale_secretPfi\",\"identifier\":\"scale_secret\",\"isEntry\":true,\"codeBytes\":384,\"instructionEstimate\":24,\"registers\":8,\"constantBytes\":364,\"sharedBytes\":null,\"localBytes\":null,\"entry\":1,\"arch\":\"sm_80\"},{\"name\":\"_Z5saxpyifPKfPf\",\"identifier\":\"saxpy\",\"isEntry\":true,\"codeBytes\":384,\"instructionEstimate\":24,\"registers\":10,\"constantBytes\":376,\"sharedBytes\":null,\"localBytes\":null,\"entry\":1,\"arch\":\"sm_80\"}],\"ptx\":[{\"version\":\"9.0\",\"target\":\"sm_80\",\"addressSize\":64,\"entries\":[{\"name\":\"_Z5saxpyifPKfPf\",\"params\":4},{\"name\":\"_Z12scale_secretPfi\",\"params\":2}],\"functions\":[],\"scanTruncated\":false,\"entry\":2}],\"warnings\":[],\"truncated\":false,\"summary\":{\"containers\":2,\"entries\":3,\"cubins\":2,\"ptxEntries\":1,\"kernels\":2,\"deviceFunctions\":0},\"redactedCount\":0,\"screened\":true}"
}
]
}
+13
View File
@@ -0,0 +1,13 @@
Parameters: {"path":"host-cuda128"}
[OK] Tool executed in 13.19ms
Result:
{
"content": [
{
"type": "text",
"text": "{\"success\":true,\"source\":\"host\",\"hostSection\":{\"name\":\".nv_fatbin\",\"offset\":8248,\"size\":6232},\"hostSignals\":[\".nv_fatbin\",\"__nv_module_id\",\".nvFatBinSegment\",\"libcudart.so.12\",\"__cudaRegisterFatBinaryEnd\",\"__cudaInitModule\",\"__cudaPopCallConfiguration\",\"__cudaRegisterFatBinary\",\"__cudaPushCallConfiguration\",\"__cudaRegisterFunction\",\"__cudaUnregisterFatBinary\"],\"containers\":2,\"entries\":[{\"index\":0,\"container\":0,\"kind\":\"cubin\",\"rawKind\":2,\"arch\":\"sm_80\",\"version\":\"0x10007\",\"flags\":\"0x11\",\"compressed\":\"none\",\"storedBytes\":872,\"size\":872,\"read\":true},{\"index\":1,\"container\":1,\"kind\":\"cubin\",\"rawKind\":2,\"arch\":\"sm_80\",\"version\":\"0x10007\",\"flags\":\"0x11\",\"compressed\":\"none\",\"storedBytes\":4456,\"size\":4456,\"read\":true},{\"index\":2,\"container\":1,\"kind\":\"ptx\",\"rawKind\":1,\"arch\":\"sm_80\",\"version\":\"0x80007\",\"flags\":\"0x2011\",\"compressed\":\"lz4\",\"storedBytes\":661,\"size\":1480,\"read\":true}],\"cubinArchs\":[\"sm_80\"],\"ptxArchs\":[\"sm_80\"],\"kernels\":[{\"name\":\"_Z12scale_secretPfi\",\"identifier\":\"scale_secret\",\"isEntry\":true,\"codeBytes\":384,\"instructionEstimate\":null,\"registers\":8,\"constantBytes\":364,\"sharedBytes\":null,\"localBytes\":null,\"entry\":1,\"arch\":\"sm_80\"},{\"name\":\"_Z5saxpyifPKfPf\",\"identifier\":\"saxpy\",\"isEntry\":true,\"codeBytes\":384,\"instructionEstimate\":null,\"registers\":10,\"constantBytes\":376,\"sharedBytes\":null,\"localBytes\":null,\"entry\":1,\"arch\":\"sm_80\"}],\"ptx\":[{\"version\":\"8.7\",\"target\":\"sm_80\",\"addressSize\":64,\"entries\":[{\"name\":\"_Z5saxpyifPKfPf\",\"params\":4},{\"name\":\"_Z12scale_secretPfi\",\"params\":2}],\"functions\":[],\"scanTruncated\":false,\"entry\":2}],\"warnings\":[\"entry 0: the entry header says sm_80, the cubin's e_flags say sm_5\",\"entry 1: the entry header says sm_80, the cubin's e_flags say sm_5\"],\"truncated\":false,\"summary\":{\"containers\":2,\"entries\":3,\"cubins\":2,\"ptxEntries\":1,\"kernels\":2,\"deviceFunctions\":0},\"redactedCount\":0,\"screened\":true}"
}
]
}
@@ -0,0 +1,13 @@
Parameters: {"path":"host-multiarch"}
[OK] Tool executed in 13.93ms
Result:
{
"content": [
{
"type": "text",
"text": "{\"success\":true,\"source\":\"host\",\"hostSection\":{\"name\":\".nv_fatbin\",\"offset\":8248,\"size\":14312},\"hostSignals\":[\".nv_fatbin\",\"__nv_module_id\",\".nvFatBinSegment\",\"libcudart.so.13\",\"__cudaGetKernel\",\"__cudaRegisterFatBinaryEnd\",\"__cudaRegisterFatBinary\",\"__cudaRegisterFunction\",\"__cudaPopCallConfiguration\",\"__cudaUnregisterFatBinary\",\"__cudaPushCallConfiguration\",\"__cudaInitModule\",\"__cudaLaunchKernel\"],\"containers\":2,\"entries\":[{\"index\":0,\"container\":0,\"kind\":\"cubin\",\"rawKind\":2,\"arch\":\"sm_75\",\"version\":\"0x10008\",\"flags\":\"0x11\",\"compressed\":\"none\",\"storedBytes\":1464,\"size\":1464,\"read\":true},{\"index\":1,\"container\":0,\"kind\":\"cubin\",\"rawKind\":2,\"arch\":\"sm_90\",\"version\":\"0x10008\",\"flags\":\"0x11\",\"compressed\":\"none\",\"storedBytes\":1544,\"size\":1544,\"read\":true},{\"index\":2,\"container\":1,\"kind\":\"cubin\",\"rawKind\":2,\"arch\":\"sm_75\",\"version\":\"0x10008\",\"flags\":\"0x11\",\"compressed\":\"none\",\"storedBytes\":4712,\"size\":4712,\"read\":true},{\"index\":3,\"container\":1,\"kind\":\"cubin\",\"rawKind\":2,\"arch\":\"sm_90\",\"version\":\"0x10008\",\"flags\":\"0x11\",\"compressed\":\"none\",\"storedBytes\":5728,\"size\":5728,\"read\":true},{\"index\":4,\"container\":1,\"kind\":\"ptx\",\"rawKind\":1,\"arch\":\"sm_90\",\"version\":\"0x90000\",\"flags\":\"0x8011\",\"compressed\":\"zstd\",\"storedBytes\":489,\"size\":1464,\"read\":true}],\"cubinArchs\":[\"sm_75\",\"sm_90\"],\"ptxArchs\":[\"sm_90\"],\"kernels\":[{\"name\":\"_Z12scale_secretPfi\",\"identifier\":\"scale_secret\",\"isEntry\":true,\"codeBytes\":256,\"instructionEstimate\":16,\"registers\":8,\"constantBytes\":364,\"sharedBytes\":null,\"localBytes\":null,\"entry\":2,\"arch\":\"sm_75\"},{\"name\":\"_Z5saxpyifPKfPf\",\"identifier\":\"saxpy\",\"isEntry\":true,\"codeBytes\":256,\"instructionEstimate\":16,\"registers\":10,\"constantBytes\":376,\"sharedBytes\":null,\"localBytes\":null,\"entry\":2,\"arch\":\"sm_75\"},{\"name\":\"_Z12scale_secretPfi\",\"identifier\":\"scale_secret\",\"isEntry\":true,\"codeBytes\":384,\"instructionEstimate\":24,\"registers\":8,\"constantBytes\":540,\"sharedBytes\":null,\"localBytes\":null,\"entry\":3,\"arch\":\"sm_90\"},{\"name\":\"_Z5saxpyifPKfPf\",\"identifier\":\"saxpy\",\"isEntry\":true,\"codeBytes\":512,\"instructionEstimate\":32,\"registers\":10,\"constantBytes\":552,\"sharedBytes\":null,\"localBytes\":null,\"entry\":3,\"arch\":\"sm_90\"}],\"ptx\":[{\"version\":\"9.0\",\"target\":\"sm_90\",\"addressSize\":64,\"entries\":[{\"name\":\"_Z5saxpyifPKfPf\",\"params\":4},{\"name\":\"_Z12scale_secretPfi\",\"params\":2}],\"functions\":[],\"scanTruncated\":false,\"entry\":4}],\"warnings\":[],\"truncated\":false,\"summary\":{\"containers\":2,\"entries\":5,\"cubins\":4,\"ptxEntries\":1,\"kernels\":4,\"deviceFunctions\":0},\"redactedCount\":0,\"screened\":true}"
}
]
}
@@ -0,0 +1,148 @@
/**
* ruRE CUDA end to end (ADR-497): the CUDA row and DETECT's CUDA routes, as the console runs them, against the REAL built ruflo CLI and the real
* nvcc binaries committed for the CLI's own tests (v3/@claude-flow/cli/__tests__/fixtures/cuda, with cuobjdump's answers beside them). The
* reader's lines are checked against those recorded cuobjdump numbers. Skips, saying so, when the CLI is not built or predates rure_analyze_cuda.
* cd v3/@claude-flow/cli && npx tsc; then: npx vitest run --dir plugins/ruflo-console/tests rure-cuda-e2e
*/
import { spawnSync } from 'node:child_process'
import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import * as zlib from 'node:zlib'
import { join, resolve } from 'node:path'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { emptyFields } from '../hooks/data/devtools'
import { DEV, devSpec } from '../hooks/devtools'
import type { Host } from '../hooks/host'
const CLI_DIR = resolve(__dirname, '../../../v3/@claude-flow/cli')
const CLI = join(CLI_DIR, 'bin', 'cli.js')
const TOOLS = join(CLI_DIR, 'dist', 'src', 'mcp-tools', 'rure-tools.js')
const FIXTURES = join(CLI_DIR, '__tests__', 'fixtures', 'cuda')
const HAVE_CUDA_TOOL = existsSync(CLI) && existsSync(TOOLS) && readFileSync(TOOLS, 'utf8').includes('rure_analyze_cuda')
if (!HAVE_CUDA_TOOL) console.log('SKIP ruRE CUDA e2e: build the CLI first (cd v3/@claude-flow/cli && npx tsc); it needs rure_analyze_cuda')
/** The CLI runs under this same Node: where it has no zstd (before 22.15; CI's Test Suite job is Node 20) a zstd entry must read as unread, not as data. */
const HAVE_ZSTD = typeof (zlib as unknown as { zstdDecompressSync?: unknown }).zstdDecompressSync === 'function'
let project = ''
/** `stat` as the host answers it, for the project. */
const fs = {
stat: async (path: string) => {
try {
const info = lstatSync(join(project, path))
return { kind: info.isDirectory() ? 'dir' : 'file', isLink: info.isSymbolicLink() }
} catch {
return undefined
}
},
}
/** A row exactly as the console runs it (its preflight may choose the reader), through the real CLI, read by the console's own reader. */
async function row(id: string, sample: string): Promise<{ lines: string[]; label: string; refused?: string }> {
const spec = devSpec(DEV.find(item => item.id === id)!, { ...emptyFields(), sample })!
const prep = await spec.preflight?.({ fs } as unknown as Host)
if (typeof prep === 'string') return { lines: [], label: spec.label, refused: prep }
const chosen = { ...spec, ...(prep ?? {}) }
const done = spawnSync(process.execPath, [CLI, ...chosen.args], { cwd: project, encoding: 'utf8', timeout: 90_000, env: { ...process.env, NO_COLOR: '1' } })
return { lines: chosen.lines?.(done.stdout ?? '', done.stderr ?? '') ?? [], label: chosen.label }
}
/** cuobjdump's recorded `-res-usage`: function -> registers and constant bank 0, per arch. */
function recorded(name: string): Record<string, Record<string, { reg: number; constant0: number }>> {
const out: Record<string, Record<string, { reg: number; constant0: number }>> = {}
for (const block of readFileSync(join(FIXTURES, `${name}.cuobjdump.txt`), 'utf8').split('Fatbin elf code:').slice(1)) {
const arch = /arch = (sm_\d+)/.exec(block)?.[1] as string
out[arch] ??= {}
for (const m of block.matchAll(/Function (\S+):\s+REG:(\d+) .*?CONSTANT\[0\]:(\d+)/g)) (out[arch] as Record<string, { reg: number; constant0: number }>)[m[1] as string] = { reg: Number(m[2]), constant0: Number(m[3]) }
}
return out
}
beforeAll(() => {
if (!HAVE_CUDA_TOOL) return
project = mkdtempSync(join(tmpdir(), 'ruflo-console-cuda-e2e-'))
mkdirSync(join(project, 'gpu'), { recursive: true })
for (const name of ['host-default', 'host-multiarch', 'host-cuda128', 'host-cuda128-all', 'k80.cubin', 'k80.fatbin', 'k80.ptx']) copyFileSync(join(FIXTURES, name), join(project, 'gpu', name))
})
afterAll(() => {
if (project !== '') rmSync(project, { recursive: true, force: true })
})
describe.skipIf(!HAVE_CUDA_TOOL)('the CUDA row against the real CLI', () => {
for (const name of ['host-default', 'host-multiarch', 'host-cuda128', 'host-cuda128-all']) {
it(`${name}: every kernel's registers and constant bank in the lines equal cuobjdump's`, async () => {
const { lines } = await row('dt-re-cuda', `gpu/${name}`)
const all = lines.join('\n')
expect(lines.some(line => line.startsWith('CUDA · host'))).toBe(true)
for (const [arch, fns] of Object.entries(recorded(name))) {
for (const [fn, expected] of Object.entries(fns)) {
const plain = /^_Z\d+(\w+?)(?:Pf|if|Pfi|[A-Z])/.exec(fn)?.[1] ?? fn
const identifier = fn.includes('scale_secret') ? 'scale_secret' : fn.includes('saxpy') ? 'saxpy' : plain
expect(all, `${identifier} in ${arch}`).toContain(` ${identifier} · ${arch} · ${expected.reg} reg`)
expect(all, `${identifier} constant in ${arch}`).toMatch(new RegExp(` ${identifier} · ${arch} · ${expected.reg} reg · \\d+ B code.* · const ${expected.constant0} B`))
}
}
})
}
it('LZ4 (CUDA 12) PTX is read on every Node', async () => {
const lz4 = (await row('dt-re-cuda', 'gpu/host-cuda128')).lines.join('\n')
expect(lz4).toMatch(/ptx sm_80 · lz4 · 1480 B/)
expect(lz4).toMatch(/PTX 8\.\d · sm_80 · 64-bit · kernels:/)
})
it(HAVE_ZSTD ? 'zstd (CUDA 13) PTX is read' : 'zstd (CUDA 13) PTX is shown as unread, with the reason, on a Node without zstd (the rest still reads)', async () => {
const zstd = (await row('dt-re-cuda', 'gpu/host-default')).lines.join('\n')
expect(zstd).toMatch(/kernels \(2\):/)
expect(zstd).toContain(' saxpy · sm_80 · 10 reg')
if (HAVE_ZSTD) {
expect(zstd).toMatch(/ptx sm_80 · zstd · 1464 B/)
expect(zstd).toMatch(/PTX 9\.0 · sm_80 · 64-bit · kernels:/)
} else {
expect(zstd).toMatch(/ptx sm_80 · zstd · unread: this Node cannot read zstd/)
expect(zstd).not.toMatch(/PTX 9\.0/)
}
})
it('a CUDA host binary read by the native row points at the CUDA row, and a non-CUDA file does not', async () => {
expect((await row('dt-re-bin', 'gpu/host-default')).lines.some(line => line.startsWith('holds CUDA code'))).toBe(true)
expect((await row('dt-re-bin', 'gpu/k80.cubin')).lines.some(line => line.startsWith('holds CUDA code'))).toBe(false)
})
})
describe.skipIf(!HAVE_CUDA_TOOL)('DETECT routes CUDA files to the CUDA reader through the real CLI', () => {
for (const [file, source] of [['k80.cubin', 'cubin'], ['k80.fatbin', 'fatbin'], ['k80.ptx', 'ptx']] as const) {
it(`${file} is read as CUDA code (${source})`, async () => {
const answer = await row('dt-re-auto', `gpu/${file}`)
expect(answer.label).toBe('ruRE: detect → CUDA')
expect(answer.lines.some(line => line.startsWith(`CUDA · ${source}`))).toBe(true)
})
}
it('the standalone PTX lists its kernels and target', async () => {
const all = (await row('dt-re-auto', 'gpu/k80.ptx')).lines.join('\n')
expect(all).toMatch(/PTX 9\.0 · sm_80 · 64-bit · kernels: /)
expect(all).toContain('_Z5saxpyifPKfPf')
})
})
@@ -0,0 +1,136 @@
/**
* ruRE CUDA on the console (ADR-497): the CUDA row, its reader against the REAL captured CLI answers (tests/fixtures/rure/cuda-*.txt, from
* nvcc 12.8 and 13.0 binaries built for ruflo's own CUDA fixtures), the hint a CUDA host binary gets from the native reader, a hostile
* answer, and DETECT's routes for .cubin, .fatbin and .ptx.
* npx vitest run --dir plugins/ruflo-console/tests rure-cuda-page
*/
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { devLines, emptyFields } from '../hooks/data/devtools'
import { RE_MAX_LINES } from '../hooks/data/rure'
import { detectReader } from '../hooks/data/rure-detect'
import { DEV, devSpec } from '../hooks/devtools'
const fixture = (name: string): string => readFileSync(join(__dirname, 'fixtures', 'rure', `${name}.txt`), 'utf8')
const linesOf = (id: string, name: string): string[] => devLines(id, fixture(name), '')
const wrapped = (inner: unknown) => `[INFO] Executing tool: x\n[OK] Tool executed in 6ms\nResult:\n${JSON.stringify({ content: [{ type: 'text', text: JSON.stringify(inner) }] })}\n`
describe('the CUDA row', () => {
it('calls rure_analyze_cuda with the sample, reads at once, and is on the native section', () => {
const entry = DEV.find(item => item.id === 'dt-re-cuda')!
const spec = devSpec(entry, { ...emptyFields(), sample: 'gpu/app' })!
expect(spec.args.slice(0, 4)).toEqual(['mcp', 'exec', '-t', 'rure_analyze_cuda'])
expect(JSON.parse(spec.args[5] as string)).toEqual({ path: 'gpu/app' })
expect(spec.isReadOnly).toBe(true)
expect(entry.group).toBe('rure-native')
})
})
describe('the CUDA reader on real answers', () => {
it('a CUDA 13 host binary: header, fatbin entries, kernels with what they cost, PTX', () => {
const lines = linesOf('dt-re-cuda', 'cuda-host')
const all = lines.join('\n')
expect(lines[0]).toBe('CUDA · host · cubins sm_80 · PTX sm_80 · 2 kernels · 0 device functions')
expect(all).toMatch(/fatbin in \.nv_fatbin at 0x[0-9a-f]+ \(7152 B\)/)
expect(all).toContain('2 container(s), 3 entries:')
expect(all).toMatch(/#0 cubin sm_80 · none · \d+ B/)
expect(all).toMatch(/#2 ptx sm_80 · zstd · 1464 B/)
expect(all).toContain(' saxpy · sm_80 · 10 reg · 384 B code (~24 instr) · const 376 B')
expect(all).toContain(' scale_secret · sm_80 · 8 reg · 384 B code (~24 instr) · const 364 B')
expect(all).toMatch(/PTX 9\.0 · sm_80 · 64-bit · kernels: _Z12scale_secretPfi, _Z5saxpyifPKfPf|PTX 9\.0 · sm_80 · 64-bit · kernels: _Z5saxpyifPKfPf, _Z12scale_secretPfi/)
expect(all).toMatch(/host signals \(\d+\):/)
})
it('a two-architecture binary shows every kernel per architecture with that architecture\'s registers', () => {
const all = linesOf('dt-re-cuda', 'cuda-multiarch').join('\n')
expect(all).toContain('cubins sm_75, sm_90 · PTX sm_90 · 4 kernels')
for (const arch of ['sm_75', 'sm_90']) {
expect(all).toContain(` saxpy · ${arch} · 10 reg`)
expect(all).toContain(` scale_secret · ${arch} · 8 reg`)
}
})
it('a CUDA 12.8 binary with LZ4 PTX reads the same way', () => {
const all = linesOf('dt-re-cuda', 'cuda-lz4').join('\n')
expect(all).toMatch(/ptx sm_80 · lz4 · 1480 B/)
expect(all).toMatch(/PTX 8\.\d · sm_80/)
expect(all).toContain(' saxpy · sm_80 · 10 reg')
})
it('a standalone cubin has no fatbin lines, and says what it is', () => {
const lines = linesOf('dt-re-cuda', 'cuda-cubin')
expect(lines[0]).toMatch(/^CUDA · cubin · cubins sm_80 · PTX \(none\) · 2 kernels/)
expect(lines.join('\n')).not.toContain('fatbin in')
expect(lines.join('\n')).toContain(' saxpy · sm_80 · 10 reg')
})
it('an unread entry says why, instead of showing a size', () => {
const lines = devLines('dt-re-cuda', wrapped({ success: true, source: 'host', containers: 1, entries: [{ index: 0, kind: 'ptx', arch: 'sm_90', compressed: 'zstd', read: false, size: null, note: 'this Node cannot read zstd (it needs 22.15 or later)' }], cubinArchs: [], ptxArchs: ['sm_90'], kernels: [], ptx: [], hostSignals: [], summary: { kernels: 0, deviceFunctions: 0 }, warnings: [], truncated: false, redactedCount: 0, screened: true }))
expect(lines.join('\n')).toContain('#0 ptx sm_90 · zstd · unread: this Node cannot read zstd (it needs 22.15 or later)')
expect(lines.join('\n')).toContain(' (none read)')
})
it('a not-CUDA answer keeps the tool\'s warning in front', () => {
const lines = devLines('dt-re-cuda', wrapped({ success: true, source: 'host', containers: 0, entries: [], cubinArchs: [], ptxArchs: [], kernels: [], ptx: [], hostSignals: [], summary: { kernels: 0, deviceFunctions: 0 }, warnings: ['no .nv_fatbin section: this does not look like a CUDA program (format elf)'], truncated: false, redactedCount: 0, screened: true }))
expect(lines[0]).toBe('⚠ no .nv_fatbin section: this does not look like a CUDA program (format elf)')
})
})
describe('a CUDA host binary read by the native reader', () => {
it('gets a line pointing at the CUDA row, and an ordinary ELF does not', () => {
expect(linesOf('dt-re-bin', 'binary-cuda').join('\n')).toContain('holds CUDA code (a .nv_fatbin section): run CUDA for its kernels, architectures and PTX')
expect(linesOf('dt-re-bin', 'binary-elf').join('\n')).not.toContain('holds CUDA code')
})
})
describe('a hostile CUDA file', () => {
const evil = '\u001b[31mRED\u001b[0m\u001b]52;c;aGk=\u0007‮TXT\u0000end'
it('no kernel, entry note, host signal, PTX or section name puts an escape or control character in a line', () => {
const lines = devLines('dt-re-cuda', wrapped({
success: true, source: evil, hostSection: { name: evil, offset: 1, size: 2 }, hostSignals: [evil], containers: 1,
entries: [{ index: 0, kind: evil, arch: evil, compressed: evil, read: false, size: null, note: evil }],
cubinArchs: [evil], ptxArchs: [evil],
kernels: [{ name: evil, identifier: evil, isEntry: true, arch: evil, registers: 8, codeBytes: 1, constantBytes: 1, sharedBytes: 1, instructionEstimate: 1 }],
ptx: [{ version: evil, target: evil, addressSize: 64, entries: [{ name: evil, params: 1 }] }],
summary: { kernels: 1, deviceFunctions: 0 }, warnings: [evil], truncated: false, redactedCount: 0, screened: true,
}))
expect(lines.length).toBeGreaterThan(5)
// eslint-disable-next-line no-control-regex
for (const line of lines) expect(line, JSON.stringify(line)).not.toMatch(/[\u0000-\u001f\u007f‪-‮]/)
})
it('numbers that are not measurements read n/a, and a huge answer is cut to the panel', () => {
const kernels = Array.from({ length: 5000 }, (_, i) => ({ name: `k${i}`, identifier: null, isEntry: true, arch: 'sm_80', registers: 1e300, codeBytes: 1e300, constantBytes: 1e300, sharedBytes: 0, instructionEstimate: 1e300 }))
const lines = devLines('dt-re-cuda', wrapped({ success: true, source: 'host', containers: 1e300, entries: [], cubinArchs: [], ptxArchs: [], kernels, ptx: [], hostSignals: [], summary: { kernels: 1e300, deviceFunctions: 0 }, warnings: [], truncated: false, redactedCount: 0, screened: true }))
const all = lines.join('\n')
expect(lines.length).toBeLessThanOrEqual(RE_MAX_LINES)
expect(all).toContain(' k0 · sm_80 · n/a reg · n/a code · const n/a')
expect(all).toContain('… +4940 more')
expect(lines.every(line => line.length < 400)).toBe(true)
})
})
describe('DETECT routes CUDA files to the CUDA row', () => {
const fs = (kind: string) => ({ stat: async () => ({ kind }) })
it('.cubin, .fatbin and .ptx, in any case, read as CUDA code', async () => {
for (const name of ['gpu/k.cubin', 'gpu/k.fatbin', 'gpu/K.PTX']) expect(await detectReader(fs('file'), name), name).toMatchObject({ id: 'dt-re-cuda', tool: 'rure_analyze_cuda', params: { path: name } })
})
it('a folder still goes to the JS reader, and an extensionless binary still to the native one', async () => {
expect(await detectReader(fs('dir'), 'gpu.ptx')).toMatchObject({ id: 'dt-re-js' })
expect(await detectReader(fs('file'), 'gpu/app')).toMatchObject({ id: 'dt-re-bin' })
})
})
@@ -37,8 +37,8 @@ const linesOf = (id: string, name: string): string => devLines(id, fixture(name)
const wrapped = (inner: unknown, isError = false) => `[INFO] Executing tool: x\n[OK] Tool executed in 6ms\nResult:\n${JSON.stringify({ content: [{ type: 'text', text: JSON.stringify(inner) }], ...(isError && { isError: true }) }, null, 2)}\n`
describe('the ruRE catalog', () => {
it('has the six tools and DETECT, each with a unique dt-re- id, in a ruRE group, and the groups have rows', () => {
expect(RURE.map(item => item.id)).toEqual(['dt-re-auto', 'dt-re-asar', 'dt-re-js', 'dt-re-find', 'dt-re-bin', 'dt-re-net', 'dt-re-evm'])
it('has the seven tools and DETECT, each with a unique dt-re- id, in a ruRE group, and the groups have rows', () => {
expect(RURE.map(item => item.id)).toEqual(['dt-re-auto', 'dt-re-asar', 'dt-re-js', 'dt-re-find', 'dt-re-bin', 'dt-re-net', 'dt-re-cuda', 'dt-re-evm'])
expect(RURE.every(item => DEV.includes(item))).toBe(true)
expect(RURE_GROUPS.every(group => DEV_GROUPS.some(known => known.id === group.id) && RURE.some(item => item.group === group.id))).toBe(true)
})
@@ -63,6 +63,7 @@ describe('the ruRE catalog', () => {
expect(params('dt-re-bin', { sample: 'bin/tool' })).toEqual({ path: 'bin/tool', includeStrings: true })
expect(params('dt-re-net', { sample: 'lib/a.dll' })).toEqual({ path: 'lib/a.dll' })
expect(params('dt-re-evm', { sample: 'c/token.bin' })).toEqual({ path: 'c/token.bin' })
expect(params('dt-re-cuda', { sample: 'gpu/app' })).toEqual({ path: 'gpu/app' })
expect(devSpec(entry('dt-re-bin'), fields({ sample: 'bin/tool' }))?.args.slice(0, 4)).toEqual(['mcp', 'exec', '-t', 'rure_analyze_binary'])
})
@@ -0,0 +1,36 @@
# ADR 497: ruRE in the console reads CUDA code
Status: Accepted (built and tested against the real CLI and real nvcc output; ships in ruflo-console 0.47.0; needs a ruflo CLI that has `rure_analyze_cuda`, which is in `main` after 3.60.1)
Date: 2026-10-23
Builds on: ADR-494 (the ruRE page), ADR-495 (the sample link check), ADR-496 (DETECT, long lists and Claude's view). The tool itself is ruflo-rure ADR-0006.
## 1. Context
`rure_analyze_cuda` reads the GPU code in a CUDA program: the fatbin entries (PTX or cubin, target architecture, compression), each kernel with its register count, code size and constant, shared and local memory, and the PTX version, target and kernel names. A person using the console could not reach it: the ruRE page lists the six earlier readers, and DETECT sent a `.cubin`, a `.fatbin` or a `.ptx` to the native binary reader, which named the format and nothing else. A CUDA host binary read by that reader showed `.nv_fatbin` among its sections with no hint that anything lay inside.
## 2. Decision
- **A CUDA row** (`dt-re-cuda`) in the native section of the ruRE page, built like its neighbours: one `ruflo mcp exec -t rure_analyze_cuda -p {path}` call with fixed argv, a read that runs at once, the same `sample` field rule, the same link check (ADR-495), the same confirm gate and palette id (`/ruflo run dt-re-cuda <path>`), so Claude reaches it through `console_run` and sees the result in `console_state`'s `rure` block (ADR-496) with no new code.
- **A reader** (`cudaLines` in `data/rure.ts`): a header line (source, the cubin architectures, the PTX architectures, kernel and device-function counts), the fatbin location and each entry (`#2 ptx sm_80 · zstd · 1464 B`, or `unread:` with the reason), one line per kernel (`saxpy · sm_80 · 10 reg · 384 B code (~24 instr) · const 376 B`, with shared memory when there is some, `[device fn]` for a function that is not launchable), the PTX modules, and the host signals. At most 30 entries and 60 kernels are shown with a count of the rest, inside the panel's 120-line cap. Every value goes through `plain()` and every number through `measureOf`: a count that is not a measurement reads `n/a`, never a plausible clamp (#3822). The tool's own warnings and what AIDefence held back come first, as for every ruRE reader.
- **DETECT routes** `.cubin`, `.fatbin` and `.ptx` (any case) to the CUDA row. A folder is still read as JavaScript source and an extensionless file as a native binary, as ADR-496 says.
- **A hint from the native row.** When the native reader's sections include `.nv_fatbin`, `.nv_fatb` or `__nv_fatbin`, it adds one line: it holds CUDA code, run CUDA. DETECT cannot see sections, so this is how a CUDA host binary, which it must read as a native binary, leads to the CUDA reader.
- **No new trust.** The path rule, the link check, the read-only class, the escape stripping and the untrusted-text marker for Claude are the ones the other rows have. Nothing is run, written or loaded.
## 3. Limits, stated plainly
- **An older CLI has no such tool.** The row then prints the existing "update ruflo" line (`NO_TOOLS`). Releasing the console before the CLI is harmless and not useful until the CLI follows.
- **The reader shows what the tool returns**: no SASS, no `.nv.info` attributes beyond the register count, no full C++ demangling (ruflo-rure ADR-0006 section "Out of scope"). Instruction counts are an estimate from code size, and the row says `~`.
- **A kernel in an architecture the tool could not read** (a payload it could not decompress) is not listed; its entry line says `unread:` and why.
- **DETECT's CUDA routes are by extension.** A CUDA binary with another extension is read as a native binary, and the hint line then points at the CUDA row.
## 4. Verification
- `tests/rure-cuda-page.spec.ts` (12): the row's call and class; the reader against four real captured CLI answers (a CUDA 13 host binary with zstd PTX, a two-architecture binary, a CUDA 12.8 binary with LZ4 PTX, a standalone cubin); an unread entry and a not-CUDA answer; the hint on a real CUDA host binary's native reading and its absence on an ordinary ELF; a hostile answer (escapes in every string field, 1e300 in every number, 5000 kernels cut to the panel); DETECT's three routes and its two unchanged ones.
- `tests/rure-cuda-e2e.spec.ts` (10): the real built CLI on the committed nvcc binaries (the CLI's own fixtures, with `cuobjdump` output recorded beside them). Every kernel's registers and constant bank in the console's lines equal `cuobjdump -res-usage`; LZ4 and zstd PTX are both read; the native row's hint appears; DETECT routes a real `.cubin`, `.fatbin` and `.ptx`. It skips, saying so, when the CLI is built without the tool. Line checks look for a line anywhere, not at index 0: without AIDefence (as in CI) a "not screened" caveat comes first.
- `tests/rure-page.spec.ts` and `tests/devtools.spec.ts` gained the row and the tool's input schema, so the catalog invariants (a `mcp exec` call names a real tool and only declared parameters; a read runs at once) cover it.
## 5. Not decided here
The folder allowlist outside the project and the ruflo-mods mod that offers to analyze new binaries remain as in ADR-496. A per-kernel drill-down (the PTX of one kernel on the page) would be a change to what the page shows, and needs the tool's `includePtx` output to be shown as text the person can read: its own decision.