@eyeauras/insight-cli 0.3.2 → 0.4.0
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/.cli-package-build.json +1 -1
- package/SKILL.md +4 -1
- package/dist/src/arguments.js +9 -4
- package/dist/src/cli.js +45 -37
- package/dist/src/code-references.d.ts +48 -0
- package/dist/src/code-references.js +154 -0
- package/dist/src/context-diagnostics.d.ts +25 -0
- package/dist/src/context-diagnostics.js +82 -0
- package/dist/src/contexts.d.ts +13 -1
- package/dist/src/contexts.js +38 -9
- package/dist/src/function-boundaries.d.ts +9 -0
- package/dist/src/function-boundaries.js +68 -0
- package/dist/src/function-entries.d.ts +33 -0
- package/dist/src/function-entries.js +84 -0
- package/dist/src/function-inspection.d.ts +4 -2
- package/dist/src/function-inspection.js +94 -59
- package/dist/src/function-results.d.ts +3 -1
- package/dist/src/function-results.js +9 -3
- package/dist/src/inline-script.d.ts +15 -0
- package/dist/src/inline-script.js +62 -0
- package/dist/src/internal-timing.d.ts +3 -0
- package/dist/src/internal-timing.js +52 -0
- package/dist/src/lifecycle.js +14 -4
- package/dist/src/native-inputs.d.ts +1 -1
- package/dist/src/native-inputs.js +1 -1
- package/dist/src/native-queries.js +1 -1
- package/dist/src/persistent-client.d.ts +30 -0
- package/dist/src/persistent-client.js +125 -0
- package/dist/src/queries.js +4 -1
- package/dist/src/query-policy.js +9 -1
- package/dist/src/research-view.d.ts +6 -0
- package/dist/src/research-view.js +9 -0
- package/dist/src/research.d.ts +38 -0
- package/dist/src/research.js +78 -8
- package/dist/src/string-anchors.d.ts +43 -0
- package/dist/src/string-anchors.js +176 -0
- package/dist/src/sync-policy.js +3 -0
- package/dist/src/timed-backend.d.ts +3 -0
- package/dist/src/timed-backend.js +16 -0
- package/dist/src/transport.d.ts +1 -0
- package/dist/src/transport.js +4 -0
- package/dist/src/workspace.js +5 -2
- package/package.json +12 -1
- package/references/native/InsightAnchorScan.java +135 -0
- package/references/native/InsightCodeReferences.java +129 -0
- package/references/native/InsightFunctionBoundaries.java +56 -0
- package/references/native/InsightInlineExecutor.java +143 -0
- package/references/native/InsightInlineScript.java +14 -0
- package/references/native/InsightPeRanges.java +92 -0
- package/references/native/InsightResearchQueries.java +22 -1
- package/references/native/InsightResearchView.java +69 -0
- package/references/native/InsightStringAnchors.java +49 -0
- package/references/persistent-client.mjs +125 -0
- package/references/persistent.md +50 -0
- package/references/query-semantics.md +12 -0
- package/references/recovery.md +36 -0
- package/references/research.md +87 -0
- package/references/setup.md +1 -1
- package/references/sparse-navigation.md +173 -0
- package/runtime/GhidraMCP-7.0.0.jar +0 -0
- package/runtime/GhidraMCP-NOTICE +5 -0
- package/runtime/provenance.json +17 -3
- package/runtime/research-view.provenance.json +28 -0
- package/runtime/toolbox.json +10 -3
- package/skill-bundle/insight-artifacts/SKILL.md +1 -1
- package/skill-bundle/insight-artifacts/references/recovery.md +36 -0
- package/skill-bundle/insight-binary/SKILL.md +1 -1
- package/skill-bundle/insight-capture/SKILL.md +1 -1
- package/skill-bundle/insight-cli/SKILL.md +4 -1
- package/skill-bundle/insight-cli/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-cli/references/persistent.md +50 -0
- package/skill-bundle/insight-cli/references/recovery.md +36 -0
- package/skill-bundle/insight-cli/references/setup.md +1 -1
- package/skill-bundle/insight-die/SKILL.md +1 -1
- package/skill-bundle/insight-dnspy/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-dump/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-stack/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-trace/SKILL.md +1 -1
- package/skill-bundle/insight-frida/SKILL.md +1 -1
- package/skill-bundle/insight-ghidra/SKILL.md +14 -1
- package/skill-bundle/insight-ghidra/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-ghidra/references/persistent.md +50 -0
- package/skill-bundle/insight-ghidra/references/query-semantics.md +12 -0
- package/skill-bundle/insight-ghidra/references/recovery.md +36 -0
- package/skill-bundle/insight-ghidra/references/research.md +87 -0
- package/skill-bundle/insight-ghidra/references/setup.md +1 -1
- package/skill-bundle/insight-ghidra/references/sparse-navigation.md +173 -0
- package/skill-bundle/insight-ilspy/SKILL.md +1 -1
- package/skill-bundle/insight-interop/SKILL.md +1 -1
- package/skill-bundle/insight-lldb/SKILL.md +1 -1
- package/skill-bundle/insight-llvm/SKILL.md +1 -1
- package/skill-bundle/insight-managed-heap/SKILL.md +1 -1
- package/skill-bundle/insight-managed-metadata/SKILL.md +1 -1
- package/skill-bundle/insight-mitmproxy/SKILL.md +1 -1
- package/skill-bundle/insight-overview/SKILL.md +1 -1
- package/skill-bundle/insight-procmon/SKILL.md +1 -1
- package/skill-bundle/insight-re/SKILL.md +4 -1
- package/skill-bundle/insight-re/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-re/references/persistent.md +50 -0
- package/skill-bundle/insight-re/references/recovery.md +36 -0
- package/skill-bundle/insight-sevenzip/SKILL.md +1 -1
- package/skill-bundle/insight-strace/SKILL.md +1 -1
- package/skill-bundle/insight-tshark/SKILL.md +1 -1
- package/skill-bundle/insight-windbg/SKILL.md +1 -1
- package/skill-bundle/manifest.json +1 -1
- package/skills/insight-artifacts/SKILL.md +1 -1
- package/skills/insight-artifacts/references/recovery.md +36 -0
- package/skills/insight-binary/SKILL.md +1 -1
- package/skills/insight-capture/SKILL.md +1 -1
- package/skills/insight-die/SKILL.md +1 -1
- package/skills/insight-dnspy/SKILL.md +1 -1
- package/skills/insight-dotnet-dump/SKILL.md +1 -1
- package/skills/insight-dotnet-stack/SKILL.md +1 -1
- package/skills/insight-dotnet-trace/SKILL.md +1 -1
- package/skills/insight-frida/SKILL.md +1 -1
- package/skills/insight-ghidra/SKILL.md +14 -1
- package/skills/insight-ghidra/references/persistent-client.mjs +125 -0
- package/skills/insight-ghidra/references/persistent.md +50 -0
- package/skills/insight-ghidra/references/query-semantics.md +12 -0
- package/skills/insight-ghidra/references/recovery.md +36 -0
- package/skills/insight-ghidra/references/research.md +87 -0
- package/skills/insight-ghidra/references/setup.md +1 -1
- package/skills/insight-ghidra/references/sparse-navigation.md +173 -0
- package/skills/insight-ilspy/SKILL.md +1 -1
- package/skills/insight-interop/SKILL.md +1 -1
- package/skills/insight-lldb/SKILL.md +1 -1
- package/skills/insight-llvm/SKILL.md +1 -1
- package/skills/insight-managed-heap/SKILL.md +1 -1
- package/skills/insight-managed-metadata/SKILL.md +1 -1
- package/skills/insight-mitmproxy/SKILL.md +1 -1
- package/skills/insight-overview/SKILL.md +1 -1
- package/skills/insight-procmon/SKILL.md +1 -1
- package/skills/insight-re/SKILL.md +4 -1
- package/skills/insight-re/references/persistent-client.mjs +125 -0
- package/skills/insight-re/references/persistent.md +50 -0
- package/skills/insight-re/references/recovery.md +36 -0
- package/skills/insight-sevenzip/SKILL.md +1 -1
- package/skills/insight-strace/SKILL.md +1 -1
- package/skills/insight-tshark/SKILL.md +1 -1
- package/skills/insight-windbg/SKILL.md +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, writeFile, readFile, open } from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { object, fail } from './model.js';
|
|
6
|
+
export const inlineSourceHash = (code) => createHash('sha256').update(code, 'utf8').digest('hex');
|
|
7
|
+
/** Native facts come from the fixed helper's receipt, never from caller console output. */
|
|
8
|
+
export function inlineReceipt(raw, expected) {
|
|
9
|
+
const r = object(raw), execution = object(r.execution), build = object(r.compilerOutput);
|
|
10
|
+
if (r.protocol !== 'insight-inline-v1' || r.attemptId !== expected.attemptId || r.requestId !== expected.requestId || r.sourceSha256 !== expected.sourceSha256 ||
|
|
11
|
+
typeof r.success !== 'boolean' || !['prepare', 'compile', 'load', 'initialize', 'construct', 'execute'].includes(String(execution.stage)) ||
|
|
12
|
+
!['not_started', 'may_have_started'].includes(String(execution.userCode)) || ![false, null].includes(execution.entrypointInvoked) ||
|
|
13
|
+
typeof build.text !== 'string' || Buffer.byteLength(build.text) > 32772 || typeof build.truncated !== 'boolean' ||
|
|
14
|
+
typeof r.console_output !== 'string' || Buffer.byteLength(r.console_output) > 32772 || typeof r.outputTruncated !== 'boolean')
|
|
15
|
+
fail('partial', 'Native inline-script receipt is absent, malformed or belongs to another attempt.');
|
|
16
|
+
if (r.success === false) {
|
|
17
|
+
const d = object(r.diagnostic);
|
|
18
|
+
if (typeof d.kind !== 'string' || typeof d.message !== 'string' || d.message.length > 2048)
|
|
19
|
+
fail('partial', 'Native inline-script failure omitted its diagnostic.');
|
|
20
|
+
}
|
|
21
|
+
if (r.success === true && (execution.userCode !== 'may_have_started' || execution.stage !== 'execute'))
|
|
22
|
+
fail('partial', 'Native inline-script completion contradicts its execution facts.');
|
|
23
|
+
const safe = r.success === false && execution.userCode === 'not_started' && execution.entrypointInvoked === false && ['prepare', 'compile', 'load'].includes(String(execution.stage));
|
|
24
|
+
if (execution.userCode === 'not_started' && !safe)
|
|
25
|
+
fail('partial', 'Native inline-script non-execution facts contradict their stage.');
|
|
26
|
+
return { response: r, safe };
|
|
27
|
+
}
|
|
28
|
+
export async function runInline(http, program, root, parameters, requestId) {
|
|
29
|
+
if (typeof parameters.code !== 'string' || Buffer.byteLength(parameters.code) > 1048576 || parameters.code.includes('\0'))
|
|
30
|
+
fail('invalid', 'Inline Java source must contain at most one MiB of UTF-8 text without NUL.');
|
|
31
|
+
const attemptId = randomUUID(), sourceSha256 = inlineSourceHash(parameters.code), directory = join(root, 'inline-attempts', attemptId);
|
|
32
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
33
|
+
const request = join(directory, 'request.json');
|
|
34
|
+
await writeFile(request, JSON.stringify({ attemptId, requestId, sourceSha256, code: parameters.code, args: parameters.args ?? '' }), { mode: 0o600, flag: 'wx' });
|
|
35
|
+
const name = 'InsightInlineScript_' + randomUUID().replaceAll('-', ''), source = await readFile(new URL('../../references/native/InsightInlineScript.java', import.meta.url), 'utf8');
|
|
36
|
+
// Only this fixed shim enters the upstream provider. User source is compiled separately.
|
|
37
|
+
const ack = object(await http.call('/run_script_inline', { program, code: source.replace('public class InsightInlineScript ', 'public class ' + name + ' '), args: Buffer.from(request, 'utf8').toString('base64') }));
|
|
38
|
+
if (ack.success !== true || typeof ack.console_output !== 'string' || !ack.console_output.includes('INSIGHT_INLINE_COMPLETE'))
|
|
39
|
+
fail('partial', 'Native inline-script executor did not acknowledge its completed attempt.');
|
|
40
|
+
const handle = await open(join(directory, 'receipt.json'), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
41
|
+
let raw;
|
|
42
|
+
try {
|
|
43
|
+
const maximum = 1048576, stat = await handle.stat();
|
|
44
|
+
if (!stat.isFile() || stat.size > maximum)
|
|
45
|
+
fail('partial', 'Native inline-script receipt exceeds its bound.');
|
|
46
|
+
const bytes = Buffer.alloc(maximum + 1);
|
|
47
|
+
let length = 0;
|
|
48
|
+
while (length < bytes.length) {
|
|
49
|
+
const next = await handle.read(bytes, length, bytes.length - length, null);
|
|
50
|
+
if (!next.bytesRead)
|
|
51
|
+
break;
|
|
52
|
+
length += next.bytesRead;
|
|
53
|
+
}
|
|
54
|
+
if (length > maximum)
|
|
55
|
+
fail('partial', 'Native inline-script receipt exceeds its bound.');
|
|
56
|
+
raw = JSON.parse(bytes.subarray(0, length).toString('utf8'));
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await handle.close();
|
|
60
|
+
}
|
|
61
|
+
return inlineReceipt(raw, { attemptId, requestId, sourceSha256 });
|
|
62
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Internal diagnostics only: fixed operation names, never argv, payloads, paths or errors. */
|
|
2
|
+
export declare function timed<T>(phase: string, operation: string, action: () => Promise<T>): Promise<T>;
|
|
3
|
+
export declare function traceCommand<T>(root: string, command: string, action: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, appendFile, stat, rename } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { performance } from 'node:perf_hooks';
|
|
6
|
+
const current = new AsyncLocalStorage(), writes = new Map();
|
|
7
|
+
/** Internal diagnostics only: fixed operation names, never argv, payloads, paths or errors. */
|
|
8
|
+
export async function timed(phase, operation, action) {
|
|
9
|
+
const trace = current.getStore();
|
|
10
|
+
if (!trace)
|
|
11
|
+
return action();
|
|
12
|
+
const span = randomUUID(), started = performance.now();
|
|
13
|
+
let outcome = 'success';
|
|
14
|
+
try {
|
|
15
|
+
return await current.run({ ...trace, parent: span }, action);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
outcome = 'failure';
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
if (trace.rows.length < 1024)
|
|
23
|
+
trace.rows.push({ trace: trace.id, span, parent: trace.parent ?? null, command: trace.command, phase, operation, startedMs: started - trace.started, durationMs: performance.now() - started, outcome });
|
|
24
|
+
else
|
|
25
|
+
trace.overflow.count++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function traceCommand(root, command, action) {
|
|
29
|
+
const trace = { id: randomUUID(), command, started: performance.now(), rows: [], overflow: { count: 0 } }, at = new Date().toISOString();
|
|
30
|
+
try {
|
|
31
|
+
return await current.run(trace, () => timed('facade.command', command, action));
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
const directory = join(root, 'diagnostics'), path = join(directory, 'timings.jsonl'), previous = writes.get(path) ?? Promise.resolve();
|
|
35
|
+
const pending = previous.catch(() => { }).then(async () => {
|
|
36
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
37
|
+
if (await stat(path).then(s => s.size, () => 0) > 8 * 1024 ** 2)
|
|
38
|
+
await rename(path, path + '.previous');
|
|
39
|
+
await appendFile(path, trace.rows.map(row => JSON.stringify({ schema: 1, at, ...row, droppedSpans: trace.overflow.count })).join('\n') + '\n', { mode: 0o600 });
|
|
40
|
+
});
|
|
41
|
+
writes.set(path, pending);
|
|
42
|
+
// Telemetry loss cannot change an acknowledged program operation into a failed write.
|
|
43
|
+
try {
|
|
44
|
+
await pending;
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
finally {
|
|
48
|
+
if (writes.get(path) === pending)
|
|
49
|
+
writes.delete(path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/src/lifecycle.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { command, Permission } from '@eyeauras/cli-factory';
|
|
1
|
+
import { command, Permission, CliError } from '@eyeauras/cli-factory';
|
|
2
2
|
import { scope } from './research.js';
|
|
3
3
|
import { id, fail, object, digest } from './model.js';
|
|
4
4
|
import { selectedOptions } from './arguments.js';
|
|
@@ -24,13 +24,23 @@ export class Lifecycle {
|
|
|
24
24
|
async change(selected, operation, callerId) {
|
|
25
25
|
const reserved = this.workspace.reserveRequest(selected, 'ghidra.' + operation, {}, callerId);
|
|
26
26
|
return this.research.ordered(selected, async () => {
|
|
27
|
-
|
|
27
|
+
let request = this.workspace.request(reserved.request.id), backend;
|
|
28
|
+
if (reserved.replay && ['running', 'unknown'].includes(request.state)) {
|
|
29
|
+
backend = await this.backend();
|
|
30
|
+
await this.research.reconcileRequests(selected, backend);
|
|
31
|
+
request = this.workspace.request(request.id);
|
|
32
|
+
}
|
|
28
33
|
if (reserved.replay && request.state !== 'accepted') {
|
|
29
34
|
if (request.state === 'applied')
|
|
30
35
|
return request.result;
|
|
31
|
-
|
|
36
|
+
throw new CliError('This lifecycle outcome remains unresolved; inspect its retained worker before retrieving it again.', {
|
|
37
|
+
code: 'coordinator.unknown', result: { ...selected, state: request.state, requestId: request.callerId ?? request.id },
|
|
38
|
+
next: [['sessions', 'status', selected.session, '--target', selected.target],
|
|
39
|
+
...(operation === 'close' ? [['tools', 'processes', '--tool', 'ghidra', '--session', selected.session, '--target', selected.target]] : [['ghidra', 'open', '--session', selected.session, '--target', selected.target]])]
|
|
40
|
+
});
|
|
32
41
|
}
|
|
33
|
-
|
|
42
|
+
backend ??= await this.backend();
|
|
43
|
+
const unresolved = this.workspace.unresolved(selected);
|
|
34
44
|
const closingExternal = operation === 'close' && unresolved.length > 0 && unresolved.every(intent => ['debugger', 'ui'].includes(String(object(intent.execution ?? {}).kind)));
|
|
35
45
|
const alreadyEnded = closingExternal && backend.closed ? await backend.closed(selected) : false;
|
|
36
46
|
if (!closingExternal)
|
|
@@ -11,7 +11,7 @@ export interface NativeInputs {
|
|
|
11
11
|
extensionRoots?: string[];
|
|
12
12
|
serverClasspath?: string;
|
|
13
13
|
}
|
|
14
|
-
export declare const pinnedMcpJar = "
|
|
14
|
+
export declare const pinnedMcpJar = "f4a9272e911466ed774c8cf73bc1adf1daef6009d895fda57aa0b28522fcdf32";
|
|
15
15
|
export declare const packagedMcpJar: string;
|
|
16
16
|
/** Ghidra has its own cache/settings overrides in addition to Java's home/tmp properties. */
|
|
17
17
|
export declare const nativeJvmDirectories: (root: string) => string[];
|
|
@@ -9,7 +9,7 @@ import { promisify } from 'node:util';
|
|
|
9
9
|
import { fail, stable, text } from './model.js';
|
|
10
10
|
import { managedExtensions, extensionJvmArguments } from './extensions.js';
|
|
11
11
|
import { runtimeValues, runtimeSelection } from './runtime-selection.js';
|
|
12
|
-
export const pinnedMcpJar = '
|
|
12
|
+
export const pinnedMcpJar = 'f4a9272e911466ed774c8cf73bc1adf1daef6009d895fda57aa0b28522fcdf32';
|
|
13
13
|
export const packagedMcpJar = fileURLToPath(new URL('../../runtime/GhidraMCP-7.0.0.jar', import.meta.url));
|
|
14
14
|
/** Ghidra has its own cache/settings overrides in addition to Java's home/tmp properties. */
|
|
15
15
|
export const nativeJvmDirectories = (root) => ['-Duser.home=' + root, '-Djava.io.tmpdir=' + root,
|
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { object, fail } from './model.js';
|
|
4
4
|
export async function nativeQuery(http, program, operation, parameters, write = false) {
|
|
5
|
-
const base = operation.startsWith('/sdk/') ? 'InsightSdkScript' : operation.startsWith('/emulate_') ? 'InsightEmulation' : 'InsightResearchQueries';
|
|
5
|
+
const base = operation === '/code_references' ? 'InsightCodeReferences' : operation.startsWith('/sdk/') ? 'InsightSdkScript' : operation.startsWith('/emulate_') ? 'InsightEmulation' : 'InsightResearchQueries';
|
|
6
6
|
const name = base + '_' + randomUUID().replaceAll('-', '');
|
|
7
7
|
const source = await readFile(new URL('../../references/native/' + base + '.java', import.meta.url), 'utf8');
|
|
8
8
|
const response = object(await http.call('/run_script_inline', { program, code: source.replace('public class ' + base + ' ', 'public class ' + name + ' '),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare class InsightCommandError extends Error {
|
|
2
|
+
readonly rpc: {
|
|
3
|
+
code: number;
|
|
4
|
+
message: string;
|
|
5
|
+
data?: unknown;
|
|
6
|
+
};
|
|
7
|
+
constructor(rpc: {
|
|
8
|
+
code: number;
|
|
9
|
+
message: string;
|
|
10
|
+
data?: unknown;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export declare class InsightTransportError extends Error {
|
|
14
|
+
readonly dispatched: boolean;
|
|
15
|
+
constructor(message: string, dispatched: boolean);
|
|
16
|
+
}
|
|
17
|
+
export interface InsightConnectionOptions {
|
|
18
|
+
executable: string;
|
|
19
|
+
/** For example [installedBin] with executable: process.execPath. No shell is used. */
|
|
20
|
+
arguments?: readonly string[];
|
|
21
|
+
cwd?: string;
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
}
|
|
24
|
+
export interface InsightConnection {
|
|
25
|
+
execute<T = unknown>(argv: readonly string[]): Promise<T>;
|
|
26
|
+
/** Drain admitted requests, then end this transport. Does not stop the owner or Ghidra. */
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/** A thin client of Factory's existing stream, with no reconnect or command replay. */
|
|
30
|
+
export declare function connectInsight(options: InsightConnectionOptions): Promise<InsightConnection>;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
3
|
+
export class InsightCommandError extends Error {
|
|
4
|
+
rpc;
|
|
5
|
+
constructor(rpc) {
|
|
6
|
+
super(rpc.message);
|
|
7
|
+
this.rpc = rpc;
|
|
8
|
+
this.name = 'InsightCommandError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class InsightTransportError extends Error {
|
|
12
|
+
dispatched;
|
|
13
|
+
constructor(message, dispatched) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.dispatched = dispatched;
|
|
16
|
+
this.name = 'InsightTransportError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** A thin client of Factory's existing stream, with no reconnect or command replay. */
|
|
20
|
+
export async function connectInsight(options) {
|
|
21
|
+
const child = spawn(options.executable, [...(options.arguments ?? []), '--json-rpc'], { cwd: options.cwd, env: options.env, stdio: ['pipe', 'pipe', 'pipe'], shell: false });
|
|
22
|
+
const maximum = 4 * 1024 ** 2, decoder = new StringDecoder('utf8');
|
|
23
|
+
let buffer = '', bytes = 0, sequence = 0, accepting = true, failed = false, ended = false;
|
|
24
|
+
let closePromise, tail = Promise.resolve();
|
|
25
|
+
const pending = new Map();
|
|
26
|
+
// Drain diagnostics without retaining arbitrary stderr, paths, tokens or unbounded output.
|
|
27
|
+
child.stderr.resume();
|
|
28
|
+
const fail = (message) => {
|
|
29
|
+
if (failed)
|
|
30
|
+
return;
|
|
31
|
+
failed = true;
|
|
32
|
+
accepting = false;
|
|
33
|
+
for (const p of pending.values())
|
|
34
|
+
p.reject(new InsightTransportError(message, true));
|
|
35
|
+
pending.clear();
|
|
36
|
+
child.stdin.destroy();
|
|
37
|
+
child.stdout.destroy();
|
|
38
|
+
};
|
|
39
|
+
child.on('error', () => fail('Persistent CLI process failed.'));
|
|
40
|
+
child.stdout.on('error', () => fail('Persistent CLI output was lost.'));
|
|
41
|
+
child.stderr.on('error', () => fail('Persistent CLI diagnostic stream was lost.'));
|
|
42
|
+
child.stdin.on('error', () => fail('Persistent CLI input was lost; inspect request/status before retrying a changing command.'));
|
|
43
|
+
const exited = new Promise((resolve, reject) => {
|
|
44
|
+
child.once('close', (code, signal) => {
|
|
45
|
+
ended = true;
|
|
46
|
+
if (code !== 0 || signal || pending.size || buffer.trim()) {
|
|
47
|
+
fail('Persistent CLI closed without a complete acknowledged response.');
|
|
48
|
+
reject(new InsightTransportError('Persistent CLI transport ended unexpectedly.', false));
|
|
49
|
+
}
|
|
50
|
+
else if (failed)
|
|
51
|
+
reject(new InsightTransportError('Persistent CLI transport failed.', false));
|
|
52
|
+
else
|
|
53
|
+
resolve();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
void exited.catch(() => { });
|
|
57
|
+
child.stdout.on('data', (chunk) => {
|
|
58
|
+
if (failed)
|
|
59
|
+
return;
|
|
60
|
+
buffer += decoder.write(chunk);
|
|
61
|
+
bytes += chunk.length;
|
|
62
|
+
if (bytes > maximum) {
|
|
63
|
+
fail('Persistent CLI response exceeded the bounded transport limit.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (;;) {
|
|
67
|
+
const newline = buffer.indexOf('\n');
|
|
68
|
+
if (newline < 0)
|
|
69
|
+
break;
|
|
70
|
+
const line = buffer.slice(0, newline);
|
|
71
|
+
buffer = buffer.slice(newline + 1);
|
|
72
|
+
bytes = Buffer.byteLength(buffer);
|
|
73
|
+
if (!line.trim())
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const r = JSON.parse(line);
|
|
77
|
+
if (!r || r.jsonrpc !== '2.0' || typeof r.id !== 'number' || !pending.has(r.id) || Object.hasOwn(r, 'result') === Object.hasOwn(r, 'error'))
|
|
78
|
+
throw Error();
|
|
79
|
+
const p = pending.get(r.id);
|
|
80
|
+
if (Object.hasOwn(r, 'error') && (!r.error || typeof r.error !== 'object' || typeof r.error.code !== 'number' || !Number.isInteger(r.error.code) || typeof r.error.message !== 'string'))
|
|
81
|
+
throw Error();
|
|
82
|
+
pending.delete(r.id);
|
|
83
|
+
if (r.error)
|
|
84
|
+
p.reject(new InsightCommandError(r.error));
|
|
85
|
+
else
|
|
86
|
+
p.resolve(r.result);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
fail('Persistent CLI returned a malformed or uncorrelated response.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', () => reject(new InsightTransportError('Could not start persistent CLI.', false))); });
|
|
95
|
+
return {
|
|
96
|
+
execute(argv) {
|
|
97
|
+
if (!Array.isArray(argv) || argv.length > 512 || argv.some(x => typeof x !== 'string' || x.includes('\0')) || Buffer.byteLength(JSON.stringify(argv)) > 1024 ** 2)
|
|
98
|
+
return Promise.reject(new TypeError('argv must be a bounded array of strings without NUL.'));
|
|
99
|
+
if (!accepting || failed || ended)
|
|
100
|
+
return Promise.reject(new InsightTransportError('Persistent CLI connection is closed.', false));
|
|
101
|
+
const args = [...argv];
|
|
102
|
+
const request = tail.then(() => {
|
|
103
|
+
if (failed || ended)
|
|
104
|
+
throw new InsightTransportError('Persistent CLI connection was lost before dispatch.', false);
|
|
105
|
+
const id = ++sequence;
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
pending.set(id, { resolve, reject });
|
|
108
|
+
try {
|
|
109
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method: 'cli.execute', params: { argv: args } }) + '\n');
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
fail('Persistent CLI input was lost; inspect request/status before retrying a changing command.');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
tail = request.catch(() => { });
|
|
117
|
+
return request;
|
|
118
|
+
},
|
|
119
|
+
close() {
|
|
120
|
+
accepting = false;
|
|
121
|
+
return closePromise ??= tail.then(async () => { if (!failed && !ended)
|
|
122
|
+
child.stdin.end(); await exited; });
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
package/dist/src/queries.js
CHANGED
|
@@ -51,7 +51,7 @@ export function queryCommands(research) {
|
|
|
51
51
|
parameters[p.name] = parsed(p, input);
|
|
52
52
|
}
|
|
53
53
|
if (q.path === '/get_functions')
|
|
54
|
-
return research(c).inspectFunctions(scope(o), parameters, { limit: number(o.limitChars ?? 16000, 32768, 1), offset: number(o.offsetChars ?? 0, 4 * 1024 ** 2), total: number(o.totalChars ?? 64000, 128000, 1), ...(o.expectView === undefined ? {} : { expectView: String(o.expectView) }), ...(o.saveOwnerDirectory === undefined ? {} : { saveDirectory: String(o.saveOwnerDirectory) }) });
|
|
54
|
+
return research(c).inspectFunctions(scope(o), parameters, { limit: number(o.limitChars ?? 16000, 32768, 1), offset: number(o.offsetChars ?? 0, 4 * 1024 ** 2), total: number(o.totalChars ?? 64000, 128000, 1), ...(o.expectView === undefined ? {} : { expectView: String(o.expectView) }), ...(o.saveOwnerDirectory === undefined ? {} : { saveDirectory: String(o.saveOwnerDirectory) }), ...(o.resolveAddress === undefined ? {} : { resolveAddress: String(o.resolveAddress) }), ...(o.maxEntryDistance === undefined ? {} : { maxEntryDistance: number(o.maxEntryDistance, 65536, 1) }), ...(o.maxUnwindBytes === undefined ? {} : { maxUnwindBytes: number(o.maxUnwindBytes, 16777216, 1) }) });
|
|
55
55
|
if (nativeQueries.has(q.path)) {
|
|
56
56
|
if (Number(parameters.timeout_seconds ?? 5) > 30)
|
|
57
57
|
fail('invalid', 'Native query timeout must be at most 30 seconds.');
|
|
@@ -65,6 +65,9 @@ export function queryCommands(research) {
|
|
|
65
65
|
await research(c).read(who, q.path, parameters);
|
|
66
66
|
return { ...who, value: value(qualifyQuery(q.path, queryResult(q.path, parameters, value(result)))) };
|
|
67
67
|
}, { permission: Permission.ReadOnly, options: [...selectedOptions, ...q.params.map(option), ...(q.path === '/get_functions' ? [
|
|
68
|
+
{ flags: '--resolve-address <mode>', description: 'containing-or-preceding or containing-or-pe-runtime; address selectors only' },
|
|
69
|
+
{ flags: '--max-entry-distance <bytes>', description: 'Required with containing-or-preceding: fallback distance 1–65536 within the same initialized executable block' },
|
|
70
|
+
{ flags: '--max-unwind-bytes <bytes>', description: 'PE runtime mode: 1–16777216 actual header/table bytes, default 4194304; fixed 5s cooperative lookup deadline' },
|
|
68
71
|
{ flags: '--limit-chars <count>', description: 'Per-function Unicode window, 1–32768', defaultValue: 16000 },
|
|
69
72
|
{ flags: '--offset-chars <count>', description: 'Per-function Unicode offset', defaultValue: 0 },
|
|
70
73
|
{ flags: '--total-chars <count>', description: 'Total returned decompiler characters, 1–128000', defaultValue: 64000 },
|
package/dist/src/query-policy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fail, object } from './model.js';
|
|
2
2
|
/** These operations have facade-owned, bounded native implementations. */
|
|
3
|
-
export const nativeQueries = new Set(['/research_view', '/decode_instructions', '/find_dead_code', '/detect_crypto_constants', '/get_valid_data_types',
|
|
3
|
+
export const nativeQueries = new Set(['/code_references', '/navigation_diagnostic', '/research_view', '/decode_instructions', '/find_dead_code', '/detect_crypto_constants', '/get_valid_data_types',
|
|
4
4
|
'/validate_data_type', '/validate_function_prototype', '/get_entry_points', '/list_globals',
|
|
5
5
|
'/list_strings', '/search_strings', '/find_data_types', '/search_byte_patterns', '/search_instructions',
|
|
6
6
|
'/emulate_function', '/emulate_hash_batch']);
|
|
@@ -25,6 +25,12 @@ const heuristics = Object.fromEntries([
|
|
|
25
25
|
'Scores and confidence labels are not calibrated probabilities; the agent decides what to accept.',
|
|
26
26
|
] }]));
|
|
27
27
|
export function queryDescription(path, fallback) {
|
|
28
|
+
if (path === '/force_decompile')
|
|
29
|
+
return 'Refresh decompiler output for an existing function entry; does not repair Listing or create functions';
|
|
30
|
+
if (path === '/disassemble_function')
|
|
31
|
+
return 'Read stored Listing instructions; for sparse/undefined code use functions decode';
|
|
32
|
+
if (path === '/get_functions')
|
|
33
|
+
return 'Inspect 1–20 functions; compact code/call edges: --fields decompiled_code,callees';
|
|
28
34
|
if (path === '/find_dead_code')
|
|
29
35
|
return 'Find blocks unreachable in the bounded static CFG; unresolved flow is reported';
|
|
30
36
|
if (path === '/detect_crypto_constants')
|
|
@@ -39,6 +45,8 @@ export function queryDescription(path, fallback) {
|
|
|
39
45
|
return 'Read external-entry metadata; report name/image-base candidates separately';
|
|
40
46
|
if (path === '/list_globals')
|
|
41
47
|
return 'Read structured global records with bounded symbol/reference scanning';
|
|
48
|
+
if (path === '/list_strings' || path === '/search_strings')
|
|
49
|
+
return 'Search defined string data only, with bounded visited-data scan; undefined literals: strings anchors';
|
|
42
50
|
if (path.startsWith('/emulate_'))
|
|
43
51
|
return 'Bounded isolated x86/x64 emulation with explicit ABI and termination reason';
|
|
44
52
|
return heuristics[path] ? fallback + ' (qualified ' + heuristics[path].kind + '; not ground truth)' : fallback;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { object, fail } from './model.js';
|
|
2
|
+
/** Strip transport versioning from the existing logical observation and its identity digest. */
|
|
3
|
+
export function researchView(raw) {
|
|
4
|
+
const r = object(raw);
|
|
5
|
+
if (r.protocol !== 'insight-research-view-v1' || typeof r.modification !== 'string' || !/^\d{1,20}$/.test(r.modification) ||
|
|
6
|
+
!['language', 'compiler'].every(k => typeof r[k] === 'string' && r[k].length > 0 && r[k].length <= 256 && !r[k].includes('\0')))
|
|
7
|
+
fail('partial', 'Native research-view observation is malformed or unsupported.');
|
|
8
|
+
return { modification: r.modification, language: r.language, compiler: r.compiler };
|
|
9
|
+
}
|
package/dist/src/research.d.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { type FunctionInspectionOptions } from './function-inspection.js';
|
|
2
|
+
import { type AnchorRequest } from './string-anchors.js';
|
|
2
3
|
import type { Artifact } from './artifacts.js';
|
|
3
4
|
import type { GhidraContext, Scope, ScalarChange, Publication, DebuggerState, GhidraLoadSettings } from './workspace.js';
|
|
4
5
|
import { Workspace } from './workspace.js';
|
|
5
6
|
import type { ToolMode } from './contexts.js';
|
|
6
7
|
import type { ScriptPage } from './scripts.js';
|
|
8
|
+
import { type ReferenceRequest } from './code-references.js';
|
|
7
9
|
export interface ResearchBackend {
|
|
10
|
+
inlineScript?(scope: Scope, parameters: Record<string, unknown>, requestId: string): Promise<{
|
|
11
|
+
response: Record<string, unknown>;
|
|
12
|
+
safe: boolean;
|
|
13
|
+
}>;
|
|
8
14
|
observation?(scope: Scope): Promise<unknown>;
|
|
15
|
+
inspectObservation?(scope: Scope): Promise<unknown>;
|
|
9
16
|
sdkFile?(operation: string, p: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
10
17
|
archivePayloads?(scope: Scope, p: Record<string, unknown>): Promise<unknown>;
|
|
11
18
|
debuggerStart?(scope: Scope): Promise<void>;
|
|
@@ -166,6 +173,36 @@ export declare class Research {
|
|
|
166
173
|
maximumCharacters: number;
|
|
167
174
|
returnedCharacters: number;
|
|
168
175
|
};
|
|
176
|
+
boundaryLookup?: Record<string, unknown> | undefined;
|
|
177
|
+
};
|
|
178
|
+
session: string;
|
|
179
|
+
target: string;
|
|
180
|
+
}>;
|
|
181
|
+
stringAnchors(selected: Scope, request: AnchorRequest): Promise<{
|
|
182
|
+
value: {
|
|
183
|
+
view: {
|
|
184
|
+
identity: string;
|
|
185
|
+
};
|
|
186
|
+
cursor: string | null;
|
|
187
|
+
};
|
|
188
|
+
session: string;
|
|
189
|
+
target: string;
|
|
190
|
+
}>;
|
|
191
|
+
codeReferences(selected: Scope, request: ReferenceRequest): Promise<{
|
|
192
|
+
value: {
|
|
193
|
+
view: {
|
|
194
|
+
identity: string;
|
|
195
|
+
};
|
|
196
|
+
cursor: string | null;
|
|
197
|
+
};
|
|
198
|
+
session: string;
|
|
199
|
+
target: string;
|
|
200
|
+
}>;
|
|
201
|
+
navigation(selected: Scope, maximum: number): Promise<{
|
|
202
|
+
value: {
|
|
203
|
+
initialAnalysis: string;
|
|
204
|
+
priorAnalysis: string;
|
|
205
|
+
provenance: string;
|
|
169
206
|
};
|
|
170
207
|
session: string;
|
|
171
208
|
target: string;
|
|
@@ -210,6 +247,7 @@ export declare class Research {
|
|
|
210
247
|
expectedShared: string | null;
|
|
211
248
|
}): Promise<unknown>;
|
|
212
249
|
private sharedOrder;
|
|
250
|
+
reconcileRequests(selected: Scope, backend: ResearchBackend): Promise<void>;
|
|
213
251
|
private reconcile;
|
|
214
252
|
merge(selected: Scope, callerId?: string): Promise<unknown>;
|
|
215
253
|
refresh(selected: Scope, callerId?: string): Promise<unknown>;
|