@eyeauras/insight-cli 0.2.3 → 0.3.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/analysis-storage.d.ts +165 -0
- package/dist/src/analysis-storage.js +562 -0
- package/dist/src/cli.js +31 -6
- package/dist/src/contexts.d.ts +1 -0
- package/dist/src/contexts.js +9 -8
- package/dist/src/decode-results.d.ts +1 -0
- package/dist/src/decode-results.js +21 -0
- package/dist/src/function-inspection.d.ts +31 -0
- package/dist/src/function-inspection.js +78 -0
- package/dist/src/function-results.d.ts +2 -0
- package/dist/src/function-results.js +21 -0
- package/dist/src/ghidra-processes.js +5 -4
- package/dist/src/initial-model.js +2 -1
- package/dist/src/native-inputs.js +2 -2
- package/dist/src/queries.js +10 -2
- package/dist/src/query-policy.js +1 -1
- package/dist/src/research.d.ts +21 -0
- package/dist/src/research.js +14 -8
- package/dist/src/runtime-manager.js +6 -5
- package/dist/src/runtime-selection.d.ts +2 -2
- package/dist/src/runtime-selection.js +13 -6
- package/dist/src/saved-materials.js +2 -1
- package/dist/src/tool-runtime/installer.js +7 -6
- package/dist/src/tool-runtime/launch-worker.js +3 -1
- package/package.json +2 -2
- package/references/artifacts.md +1 -1
- package/references/managed-runtime.md +1 -1
- package/references/native/InsightResearchQueries.java +33 -2
- package/references/research.md +40 -0
- package/references/setup.md +4 -0
- package/references/storage.md +99 -0
- package/runtime/toolbox.json +10 -4
- package/skill-bundle/insight-artifacts/SKILL.md +1 -1
- package/skill-bundle/insight-artifacts/references/artifacts.md +1 -1
- package/skill-bundle/insight-artifacts/references/recovery.md +79 -0
- package/skill-bundle/insight-artifacts/references/storage.md +99 -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/recovery.md +79 -0
- package/skill-bundle/insight-cli/references/storage.md +99 -0
- 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 +1 -1
- package/skill-bundle/insight-ghidra/references/artifacts.md +1 -1
- package/skill-bundle/insight-ghidra/references/managed-runtime.md +1 -1
- package/skill-bundle/insight-ghidra/references/research.md +40 -0
- package/skill-bundle/insight-ghidra/references/setup.md +4 -0
- package/skill-bundle/insight-ghidra/references/storage.md +99 -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 +1 -1
- package/skill-bundle/insight-re/references/artifacts.md +1 -1
- package/skill-bundle/insight-re/references/storage.md +99 -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/artifacts.md +1 -1
- package/skills/insight-artifacts/references/recovery.md +79 -0
- package/skills/insight-artifacts/references/storage.md +99 -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 +1 -1
- package/skills/insight-ghidra/references/artifacts.md +1 -1
- package/skills/insight-ghidra/references/managed-runtime.md +1 -1
- package/skills/insight-ghidra/references/research.md +40 -0
- package/skills/insight-ghidra/references/setup.md +4 -0
- package/skills/insight-ghidra/references/storage.md +99 -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 +1 -1
- package/skills/insight-re/references/artifacts.md +1 -1
- package/skills/insight-re/references/storage.md +99 -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
package/dist/src/cli.js
CHANGED
|
@@ -29,6 +29,7 @@ import { nativeInputs } from './native-inputs.js';
|
|
|
29
29
|
import { Research, scope, address, commentKind, scalarText, number } from './research.js';
|
|
30
30
|
import { fail, id, text, digest, object } from './model.js';
|
|
31
31
|
import { queryCommands, extendQueries } from './queries.js';
|
|
32
|
+
import { decodedInstructions } from './decode-results.js';
|
|
32
33
|
import { mutationCommands } from './mutations.js';
|
|
33
34
|
import { crossCommands } from './cross.js';
|
|
34
35
|
import { Administration, administrationCommands } from './administration.js';
|
|
@@ -50,9 +51,11 @@ import { artifactsCommands } from './saved-materials.js';
|
|
|
50
51
|
import { RuntimeManager, runtimeCommands, runtimeProcesses } from './runtime-manager.js';
|
|
51
52
|
import { packageVersion } from './package-version.js';
|
|
52
53
|
import { runtimeSelection } from './runtime-selection.js';
|
|
54
|
+
import { AnalysisStorage, analysisDirectory } from './analysis-storage.js';
|
|
53
55
|
const bindingFields = ['java', 'ghidraHome', 'mcpJar', 'mcpJarSha256', 'server', 'repository', 'readerUser', 'publisherUser', 'maxContexts', 'debuggerPython', 'windbgDir', 'archiveUrl'];
|
|
54
56
|
class Owner {
|
|
55
57
|
options;
|
|
58
|
+
storage;
|
|
56
59
|
profiles = new Map();
|
|
57
60
|
runtimeChanging = new Set();
|
|
58
61
|
async runtime(c, action, quiescent = true) {
|
|
@@ -87,10 +90,11 @@ class Owner {
|
|
|
87
90
|
const directory = c.appArguments.AppDataDirectory;
|
|
88
91
|
if (this.runtimeChanging.has(directory))
|
|
89
92
|
fail('busy', 'Runtime preparation is in progress.');
|
|
90
|
-
const
|
|
93
|
+
const root = analysisDirectory(c);
|
|
94
|
+
const configuration = digest({ analysisDirectory: root, runtime: runtimeSelection(directory, root), profile: Object.fromEntries(bindingFields.map(key => [key, c.profile.values[key]])), extensions: extensionState(join(root, 'workspace')).modules.filter(m => m.enabled).map(m => [m.name, m.sha256]) });
|
|
91
95
|
let profile = this.profiles.get(directory);
|
|
92
96
|
if (!profile) {
|
|
93
|
-
profile = { workspace: new Workspace(join(
|
|
97
|
+
profile = { workspace: new Workspace(join(root, 'workspace'), c.profile.name), tasks: new Map(), configuration };
|
|
94
98
|
this.profiles.set(directory, profile);
|
|
95
99
|
}
|
|
96
100
|
if (profile.configuration !== configuration)
|
|
@@ -115,6 +119,8 @@ class Owner {
|
|
|
115
119
|
}
|
|
116
120
|
async invalidateProfile(app) {
|
|
117
121
|
const directory = app.AppDataDirectory, profile = this.profiles.get(directory);
|
|
122
|
+
if (this.storage?.busy(directory))
|
|
123
|
+
fail('busy', 'Storage migration is in progress.');
|
|
118
124
|
if (this.runtimeChanging.has(directory))
|
|
119
125
|
fail('busy', 'Runtime preparation is in progress; finish it before changing or deleting the profile.');
|
|
120
126
|
if (toolProfileBusy(directory))
|
|
@@ -123,7 +129,7 @@ class Owner {
|
|
|
123
129
|
fail('busy', 'The profile has active or unverified tool processes.');
|
|
124
130
|
const materials = await readdir(join(directory, 'materials')).catch(error => { if (error.code === 'ENOENT')
|
|
125
131
|
return []; throw error; });
|
|
126
|
-
if (existsSync(join(directory, 'workspace', 'workspace.sqlite')) || materials.length)
|
|
132
|
+
if (existsSync(join(directory, 'storage', 'migration.json')) || existsSync(join(directory, 'workspace', 'workspace.sqlite')) || materials.length)
|
|
127
133
|
fail('blocked', 'Profile has retained research data; preserve its workspace and materials before deleting or reconfiguring it.');
|
|
128
134
|
if (profile) {
|
|
129
135
|
await (await profile.backend)?.dispose();
|
|
@@ -131,6 +137,16 @@ class Owner {
|
|
|
131
137
|
this.profiles.delete(directory);
|
|
132
138
|
}
|
|
133
139
|
}
|
|
140
|
+
async releaseStorage(c) {
|
|
141
|
+
const directory = c.appArguments.AppDataDirectory, profile = this.profiles.get(directory);
|
|
142
|
+
if (!profile)
|
|
143
|
+
return;
|
|
144
|
+
if (profile.tasks.size)
|
|
145
|
+
fail('busy', 'Research commands are active.');
|
|
146
|
+
await (await profile.backend)?.dispose();
|
|
147
|
+
profile.workspace.close();
|
|
148
|
+
this.profiles.delete(directory);
|
|
149
|
+
}
|
|
134
150
|
async dispose() {
|
|
135
151
|
for (const profile of this.profiles.values()) {
|
|
136
152
|
await Promise.allSettled([...profile.tasks.values()]);
|
|
@@ -259,6 +275,8 @@ function scopedFailures(entry, owner, parents = []) {
|
|
|
259
275
|
}
|
|
260
276
|
export function createDefinition(options = {}) {
|
|
261
277
|
const owner = new Owner(options);
|
|
278
|
+
const storage = new AnalysisStorage(options.runtime?.profileStore, c => owner.releaseStorage(c));
|
|
279
|
+
owner.storage = storage;
|
|
262
280
|
const definition = {
|
|
263
281
|
name: 'insight-cli', applicationId: 'insight-cli', version: packageVersion, description: 'Isolated research contexts and coordinated Ghidra analysis',
|
|
264
282
|
resources: [owner], permissions: { categories: [{ name: 'Destructive', description: 'Explicit deletion or replacement of retained research data', enabledByDefault: false }] }, ...(options.runtime ? { runtime: options.runtime } : {}),
|
|
@@ -449,6 +467,12 @@ export function createDefinition(options = {}) {
|
|
|
449
467
|
{ flags: '--execute', description: 'Enable execution permission' }, request] }),
|
|
450
468
|
]),
|
|
451
469
|
command('functions', 'Read or name functions', [
|
|
470
|
+
command('decode', 'Decode executable bytes without defining instructions or functions', async ({ options: o }, c) => {
|
|
471
|
+
const selected = owner.selected(c), who = scope(o), at = address(o.address), maxBytes = number(o.maxBytes ?? 64, 4096, 1), maxInstructions = number(o.maxInstructions ?? 16, 256, 1);
|
|
472
|
+
const value = decodedInstructions(await selected.research.read(who, '/decode_instructions', { address: at, max_bytes: maxBytes, max_instructions: maxInstructions }), at, maxBytes, maxInstructions);
|
|
473
|
+
const context = selected.workspace.context(who);
|
|
474
|
+
return { ...who, value: { ...value, view: { revision: context.baseRevision, identity: digest({ context: context.id, generation: context.generation, snapshot: context.workingSnapshot, nativeModification: object(value.read_only).modification }) } } };
|
|
475
|
+
}, { permission: Permission.ReadOnly, options: [...scoped, at, { flags: '--max-bytes <count>', description: 'Read 1–4096 executable bytes', defaultValue: 64 }, { flags: '--max-instructions <count>', description: 'Decode at most 1–256 instructions', defaultValue: 16 }] }),
|
|
452
476
|
command('set-function-prototype', 'Set a C prototype and optional explicit compiler-supported calling convention', ({ options: o }, c) => {
|
|
453
477
|
const selected = owner.selected(c), who = scope(o), parameters = { function_address: address(o.address), prototype: text(o.prototype, 16000),
|
|
454
478
|
...(o.callingConvention === undefined ? {} : { calling_convention: text(o.callingConvention, 512) }) };
|
|
@@ -476,8 +500,9 @@ export function createDefinition(options = {}) {
|
|
|
476
500
|
]),
|
|
477
501
|
],
|
|
478
502
|
};
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
503
|
+
const commands = definition.commands?.map(entry => entry.name === 'ghidra' ?
|
|
504
|
+
scopedFailures({ ...entry, children: extendQueries(extendQueries(extendQueries(entry.children ?? [], queryCommands(c => owner.selected(c).research)), mutationCommands(c => owner.selected(c).research)), [...sdkCommands(c => owner.selected(c).research), ...sdkFileCommands(c => owner.selected(c).sdkFiles), ...trackingCommands(c => owner.selected(c).tracking), ...crossCommands(c => owner.selected(c).research), ...administrationCommands(c => owner.selected(c).administration, options.backend ? undefined : async (c) => { const status = await new RuntimeManager(c).status(); return status.state === 'not_ready' ? { ...status.server, connected: false, state: 'runtime_not_ready', missing: status.missing } : undefined; }),
|
|
505
|
+
...extensionCommands(c => new Extensions(join(analysisDirectory(c), 'workspace'))), ...extensionInventoryCommands(c => owner.selected(c).backend), ...pluginCommands(c => owner.selected(c).ui), ...synchronizationCommands(c => owner.selected(c).synchronization), ...utilityCommands(), ...runtimeCommands((c, action, quiescent) => owner.runtime(c, action, quiescent)), ...lifecycleCommands(c => owner.selected(c).lifecycle, c => new RuntimeManager(c).status()), ...projectCommands(c => owner.selected(c).projects), ...uiCommands(c => owner.selected(c).ui), ...debuggerCommands(c => owner.selected(c).debugger), ...archiveCommands(c => owner.selected(c).archive)]) }, owner) : entry);
|
|
506
|
+
return { ...definition, commands: [...(commands ?? []).map(e => storage.wrap(e)), storage.commands()] };
|
|
482
507
|
}
|
|
483
508
|
export const createInsightCli = (options = {}) => createCli(createDefinition(options));
|
package/dist/src/contexts.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export declare class GhidraContexts {
|
|
|
62
62
|
private privateCopy;
|
|
63
63
|
open(scope: Scope, request?: string): Promise<GhidraContext>;
|
|
64
64
|
private selected;
|
|
65
|
+
observation(scope: Scope): Promise<unknown>;
|
|
65
66
|
call(scope: Scope, operation: string, parameters?: Record<string, unknown>): Promise<unknown>;
|
|
66
67
|
saved(scope: Scope): Promise<Artifact>;
|
|
67
68
|
scripts(scope: Scope, filter: string, limit: number, offset: number): Promise<import("./scripts.js").ScriptPage>;
|
package/dist/src/contexts.js
CHANGED
|
@@ -167,8 +167,8 @@ export class GhidraContexts {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
async verified(binding, target, verifySource = true) {
|
|
170
|
-
const root =
|
|
171
|
-
if (!root.startsWith(resolve(this.directory()) + sep) ||
|
|
170
|
+
const root = await realpath(binding.root);
|
|
171
|
+
if (!root.startsWith(resolve(this.directory()) + sep) || binding.target !== target.id || binding.runtimeFingerprint !== this.inputs.fingerprint)
|
|
172
172
|
fail('binding', 'Native context receipt does not match its profile, target or runtime.');
|
|
173
173
|
if (stable(JSON.parse(await readFile(join(root, 'process.json'), 'utf8'))) !== stable(binding))
|
|
174
174
|
fail('binding', 'Native context receipt changed.');
|
|
@@ -219,11 +219,11 @@ export class GhidraContexts {
|
|
|
219
219
|
this.running.delete(binding.root);
|
|
220
220
|
}
|
|
221
221
|
async debuggerReleased(binding) {
|
|
222
|
-
const root =
|
|
223
|
-
if (!root.startsWith(resolve(this.directory()) + sep) ||
|
|
222
|
+
const root = await realpath(binding.root);
|
|
223
|
+
if (!root.startsWith(resolve(this.directory()) + sep) ||
|
|
224
224
|
stable(JSON.parse(await readFile(join(root, 'process.json'), 'utf8'))) !== stable(binding))
|
|
225
225
|
fail('binding', 'Debugger lifetime binding moved outside its owned context.');
|
|
226
|
-
const directory = join(
|
|
226
|
+
const directory = join(root, 'debugger-supervisors'), deadline = Date.now() + 15000;
|
|
227
227
|
for (;;) {
|
|
228
228
|
let names;
|
|
229
229
|
try {
|
|
@@ -305,7 +305,7 @@ export class GhidraContexts {
|
|
|
305
305
|
if (await realpath(root) !== resolve(root))
|
|
306
306
|
fail('binding', 'Legacy initial-import stage moved outside owned storage.');
|
|
307
307
|
const binding = JSON.parse(await readFile(join(root, 'process.json'), 'utf8'));
|
|
308
|
-
if (binding.root !== root || binding.target !== target.id || binding.kind !== 'bootstrap' || binding.program !== programPath(target.id))
|
|
308
|
+
if (await realpath(binding.root) !== root || binding.target !== target.id || binding.kind !== 'bootstrap' || binding.program !== programPath(target.id))
|
|
309
309
|
fail('binding', 'Legacy bootstrap receipt changed.');
|
|
310
310
|
let stopped = false;
|
|
311
311
|
try {
|
|
@@ -447,6 +447,7 @@ export class GhidraContexts {
|
|
|
447
447
|
fail('binding', 'Context process binding changed.');
|
|
448
448
|
return this.verified(binding, this.workspace.target(scope.target));
|
|
449
449
|
}
|
|
450
|
+
async observation(scope) { return this.call(scope, '/research_view'); }
|
|
450
451
|
async call(scope, operation, parameters = {}) {
|
|
451
452
|
if (Object.hasOwn(parameters, 'program'))
|
|
452
453
|
fail('invalid', 'Program routing is owned by the session and target selectors.');
|
|
@@ -871,8 +872,8 @@ public class ${name} extends GhidraScript {
|
|
|
871
872
|
}
|
|
872
873
|
async closed(scope) {
|
|
873
874
|
const context = this.workspace.context(scope), path = join(this.directory(), 'context', context.id, 'current.json');
|
|
874
|
-
const binding = JSON.parse(await readFile(path, 'utf8')), root =
|
|
875
|
-
if (!root.startsWith(resolve(this.directory()) + sep) ||
|
|
875
|
+
const binding = JSON.parse(await readFile(path, 'utf8')), root = await realpath(binding.root);
|
|
876
|
+
if (!root.startsWith(resolve(this.directory()) + sep) || binding.context !== context.id || binding.target !== scope.target ||
|
|
876
877
|
stable(JSON.parse(await readFile(join(root, 'process.json'), 'utf8'))) !== stable(binding))
|
|
877
878
|
fail('binding', 'Closed context receipt changed.');
|
|
878
879
|
try {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function decodedInstructions(raw: unknown, at: string, maxBytes: number, maxInstructions: number): Record<string, unknown>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { object, fail } from './model.js';
|
|
3
|
+
import { address } from './research.js';
|
|
4
|
+
export function decodedInstructions(raw, at, maxBytes, maxInstructions) {
|
|
5
|
+
const value = object(raw), input = value.input_bytes;
|
|
6
|
+
if (typeof input !== 'string' || !/^(?:[a-f0-9]{2})+$/.test(input) || input.length > maxBytes * 2 || value.input_sha256 !== createHash('sha256').update(Buffer.from(input, 'hex')).digest('hex') || address(value.address) !== at || !Array.isArray(value.instructions) || value.instructions.length > maxInstructions || typeof value.language !== 'string' || typeof value.stop_reason !== 'string' || !['byte_limit', 'instruction_limit', 'truncated_instruction', 'executable_block_end', 'terminal_flow', 'control_flow', 'context_unavailable', 'undecodable'].includes(value.stop_reason))
|
|
7
|
+
fail('partial', 'Malformed bounded instruction result.');
|
|
8
|
+
let consumed = 0;
|
|
9
|
+
for (const raw of value.instructions) {
|
|
10
|
+
const row = object(raw);
|
|
11
|
+
if (!Number.isSafeInteger(row.length) || Number(row.length) < 1 || typeof row.bytes !== 'string' || row.bytes !== input.slice(consumed * 2, (consumed + Number(row.length)) * 2) || row.bytes.length !== Number(row.length) * 2 || typeof row.mnemonic !== 'string' || !Array.isArray(row.operands) || row.operands.some(v => typeof v !== 'string') || !Array.isArray(row.direct_targets) || row.direct_targets.some(v => typeof v !== 'string') || typeof row.computed_flow !== 'boolean' || row.computed_flow && row.direct_targets.length)
|
|
12
|
+
fail('partial', 'Decoded instruction does not match input bytes or flow.');
|
|
13
|
+
const [space, offset] = at.includes(':') ? at.split(':') : [undefined, at];
|
|
14
|
+
if (address(row.address) !== (space ? space + ':' : '') + (BigInt('0x' + offset) + BigInt(consumed)).toString(16))
|
|
15
|
+
fail('partial', 'Decoded instruction address changed.');
|
|
16
|
+
consumed += Number(row.length);
|
|
17
|
+
}
|
|
18
|
+
if (value.consumed_bytes !== consumed || object(value.read_only ?? {}).modification_unchanged !== true)
|
|
19
|
+
fail('partial', 'Decoder omitted consumed bytes or read-only proof.');
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ResearchBackend } from './research.js';
|
|
2
|
+
import type { Scope } from './workspace.js';
|
|
3
|
+
export interface FunctionInspectionOptions {
|
|
4
|
+
limit: number;
|
|
5
|
+
offset: number;
|
|
6
|
+
total: number;
|
|
7
|
+
expectView?: string;
|
|
8
|
+
saveDirectory?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function inspectFunctions(backend: ResearchBackend, scope: Scope, parameters: Record<string, unknown>, options: FunctionInspectionOptions, view: {
|
|
11
|
+
revision?: string;
|
|
12
|
+
identity: string;
|
|
13
|
+
}, observation?: unknown): Promise<{
|
|
14
|
+
value: {
|
|
15
|
+
save?: {
|
|
16
|
+
host: string;
|
|
17
|
+
directory: string;
|
|
18
|
+
manifest: string;
|
|
19
|
+
} | undefined;
|
|
20
|
+
view: {
|
|
21
|
+
revision?: string;
|
|
22
|
+
identity: string;
|
|
23
|
+
};
|
|
24
|
+
textBudget: {
|
|
25
|
+
maximumCharacters: number;
|
|
26
|
+
returnedCharacters: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
session: string;
|
|
30
|
+
target: string;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve, join, isAbsolute } from 'node:path';
|
|
3
|
+
import { fail, object, digest } from './model.js';
|
|
4
|
+
import { functionResult } from './function-results.js';
|
|
5
|
+
import { textWindow } from './text-window.js';
|
|
6
|
+
import { CliError } from '@eyeauras/cli-factory';
|
|
7
|
+
const fields = new Set('signature classification return_type entry_point body_start body_end decompiled_code plate_comment comments labels tags parameters locals callers call_context callees xrefs disassembly jump_targets refs'.split(' '));
|
|
8
|
+
export async function inspectFunctions(backend, scope, parameters, options, view, observation) {
|
|
9
|
+
if (options.saveDirectory !== undefined && !isAbsolute(options.saveDirectory))
|
|
10
|
+
fail('invalid', 'Save directory must be an absolute path on the owner host.');
|
|
11
|
+
const selectors = String(parameters.functions || parameters.function || '').split(',').map(s => s.trim());
|
|
12
|
+
if (!selectors.length || selectors.length > 20 || selectors.some(s => !s || s.length > 128) || new Set(selectors).size !== selectors.length)
|
|
13
|
+
fail('invalid', 'Select 1–20 distinct function names or addresses.');
|
|
14
|
+
const selectedFields = String(parameters.fields ?? '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
15
|
+
if (selectedFields.some(s => !fields.has(s)))
|
|
16
|
+
fail('invalid', 'Unknown function field. Allowed fields: ' + [...fields].join(', '));
|
|
17
|
+
if (options.expectView !== undefined && options.expectView !== view.identity)
|
|
18
|
+
fail('binding', 'Function continuation view changed.', { reason: 'changed_view', view });
|
|
19
|
+
const raw = object(await backend.call(scope, '/get_functions', { ...parameters, functions: selectors.join(',') }));
|
|
20
|
+
if (backend.observation && digest(observation) !== digest(await backend.observation(scope)))
|
|
21
|
+
fail('binding', 'Native view changed during bulk function inspection.', { reason: 'changed_view', view });
|
|
22
|
+
const functions = object(raw.functions);
|
|
23
|
+
if (Object.keys(functions).length !== selectors.length || selectors.some(s => !Object.hasOwn(functions, s)))
|
|
24
|
+
fail('partial', 'Bulk function selectors changed.');
|
|
25
|
+
const output = Object.create(null), saved = [];
|
|
26
|
+
let remaining = options.total;
|
|
27
|
+
for (const [index, selector] of selectors.entries()) {
|
|
28
|
+
let found;
|
|
29
|
+
try {
|
|
30
|
+
found = functionResult({ functions: { [selector]: functions[selector] } }, selector);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (!(error instanceof CliError))
|
|
34
|
+
throw error;
|
|
35
|
+
const detail = object(error.result);
|
|
36
|
+
output[selector] = { error: detail.nativeDiagnostic ?? error.message, ...detail, status: detail.reason === 'function_not_found' ? 'not_found' : 'failed' };
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const row = { ...found, status: 'success' };
|
|
40
|
+
if (typeof row.decompiled_code === 'string') {
|
|
41
|
+
const source = row.decompiled_code, window = textWindow(source, options.saveDirectory ? 0 : Math.min(options.limit, remaining), options.saveDirectory ? 0 : options.offset, Boolean(object(row.truncation ?? {}).decompiled_code));
|
|
42
|
+
const { content, ...metadata } = window;
|
|
43
|
+
remaining -= Array.from(content).length;
|
|
44
|
+
row.decompiled_code = content;
|
|
45
|
+
row.text = { ...metadata, returnedCharacters: Array.from(content).length, nextOffset: Math.min(window.totalCharacters, window.offset + Array.from(content).length) };
|
|
46
|
+
if (options.saveDirectory) {
|
|
47
|
+
if (Buffer.byteLength(source) > 4 * 1024 ** 2)
|
|
48
|
+
fail('limit', 'Native function text exceeds the four MiB saved-file bound.');
|
|
49
|
+
const name = String(index).padStart(2, '0') + '-' + window.textSha256 + '.c';
|
|
50
|
+
saved.push({ name, content: source });
|
|
51
|
+
delete row.decompiled_code;
|
|
52
|
+
row.savedText = { file: join(resolve(options.saveDirectory), name), textSha256: window.textSha256, totalCharacters: window.totalCharacters, sourceTruncated: window.sourceTruncated, coverage: 'entire returned native text' };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (!selectedFields.length || selectedFields.includes('decompiled_code')) {
|
|
56
|
+
row.status = 'partial';
|
|
57
|
+
row.text = { reason: 'missing_decompiler_text' };
|
|
58
|
+
}
|
|
59
|
+
output[selector] = row;
|
|
60
|
+
}
|
|
61
|
+
if (!parameters.functions) {
|
|
62
|
+
const single = object(output[selectors[0]]);
|
|
63
|
+
if (single.status === 'failed' || single.status === 'not_found')
|
|
64
|
+
fail('partial', String(single.error), single);
|
|
65
|
+
}
|
|
66
|
+
const result = { ...(parameters.functions ? { ...raw, functions: output } : object(output[selectors[0]])), view, textBudget: { maximumCharacters: options.total, returnedCharacters: options.total - remaining }, ...(options.saveDirectory ? { save: { host: 'owner', directory: resolve(options.saveDirectory), manifest: join(resolve(options.saveDirectory), 'manifest.json') } } : {}) };
|
|
67
|
+
if (Buffer.byteLength(JSON.stringify(result)) > 1048576)
|
|
68
|
+
fail('limit', 'Function response exceeds one MiB; narrow fields or selectors.');
|
|
69
|
+
if (options.saveDirectory) {
|
|
70
|
+
// A fresh directory prevents overwrite and makes a partial filesystem failure observable.
|
|
71
|
+
const directory = resolve(options.saveDirectory);
|
|
72
|
+
await mkdir(directory, { recursive: false });
|
|
73
|
+
for (const file of saved)
|
|
74
|
+
await writeFile(join(directory, file.name), file.content, { flag: 'wx', mode: 0o600 });
|
|
75
|
+
await writeFile(join(directory, 'manifest.json'), JSON.stringify({ ...scope, value: result }, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
|
|
76
|
+
}
|
|
77
|
+
return { ...scope, value: result };
|
|
78
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fail } from './model.js';
|
|
2
|
+
const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
3
|
+
export function functionFailure(selector, reason, diagnostic) {
|
|
4
|
+
const nativeDiagnostic = typeof diagnostic === 'string' ? Array.from(diagnostic.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')).slice(0, 500).join('') : undefined;
|
|
5
|
+
fail('partial', 'Function inspection failed: ' + reason + '.', { selector, reason, ...(nativeDiagnostic ? { nativeDiagnostic } : {}) });
|
|
6
|
+
}
|
|
7
|
+
export function functionResult(value, selector) {
|
|
8
|
+
if (!record(value) || !record(value.functions))
|
|
9
|
+
functionFailure(selector, 'invalid_native_shape');
|
|
10
|
+
const functions = value.functions;
|
|
11
|
+
if (Object.keys(functions).length !== 1 || !Object.hasOwn(functions, selector))
|
|
12
|
+
functionFailure(selector, 'selector_mismatch');
|
|
13
|
+
const found = functions[selector];
|
|
14
|
+
if (!record(found))
|
|
15
|
+
functionFailure(selector, 'invalid_native_shape');
|
|
16
|
+
if (found.error !== undefined)
|
|
17
|
+
functionFailure(selector, typeof found.error === 'string' && /^Function not found\b/i.test(found.error) ? 'function_not_found' : 'native_function_failure', found.error);
|
|
18
|
+
if (typeof found.name !== 'string' || !found.name || typeof found.entry_point !== 'string' || found.entry_point.length > 128 || !/^(?:[A-Za-z0-9_]+:)?(?:0x)?[a-fA-F0-9]+$/.test(found.entry_point))
|
|
19
|
+
functionFailure(selector, 'invalid_native_shape');
|
|
20
|
+
return found;
|
|
21
|
+
}
|
|
@@ -5,16 +5,17 @@ import { serverIdentity, stopLocalServer } from './server.js';
|
|
|
5
5
|
import { HttpBridge } from './transport.js';
|
|
6
6
|
import { registerExistingProcess } from './tool-runtime/supervisor.js';
|
|
7
7
|
import { fail } from './model.js';
|
|
8
|
+
import { analysisDirectory } from './analysis-storage.js';
|
|
8
9
|
async function ownedPath(c, path) {
|
|
9
10
|
if (typeof path !== 'string')
|
|
10
11
|
fail('binding', 'Tool resource moved outside this profile.');
|
|
11
|
-
const supplied = resolve(path), profile = resolve(c.appArguments.AppDataDirectory), canonical = await realpath(profile);
|
|
12
|
-
const base = supplied.startsWith(profile + sep) ? profile : supplied.startsWith(canonical + sep) ? canonical : undefined;
|
|
12
|
+
const supplied = resolve(path), profile = resolve(c.appArguments.AppDataDirectory), canonical = await realpath(profile), analysis = await realpath(analysisDirectory(c));
|
|
13
|
+
const base = supplied.startsWith(profile + sep) ? profile : supplied.startsWith(canonical + sep) ? canonical : supplied.startsWith(analysis + sep) ? analysis : undefined;
|
|
13
14
|
if (!base)
|
|
14
15
|
fail('binding', 'Tool resource moved outside this profile.');
|
|
15
16
|
// Keep Factory's raw AppData identity in process records, but compare native resources
|
|
16
17
|
// under its canonical profile. Accept the parent alias, never a replaced child link.
|
|
17
|
-
const expected = join(
|
|
18
|
+
const suffix = relative(base, supplied), expected = base === analysis ? supplied : ['workspace', 'runtime', 'materials', 'temp'].some(tree => suffix === tree || suffix.startsWith(tree + sep)) ? join(analysis, suffix) : join(canonical, suffix);
|
|
18
19
|
if (await realpath(supplied) !== expected)
|
|
19
20
|
fail('binding', 'Tool resource moved outside this profile.');
|
|
20
21
|
return expected;
|
|
@@ -53,7 +54,7 @@ export function ghidraProcessHooks(workspace, close) {
|
|
|
53
54
|
return {
|
|
54
55
|
verify: verifyGhidraProcess,
|
|
55
56
|
async discover(c) {
|
|
56
|
-
const directory = c.appArguments.AppDataDirectory, executable = runtimeSelection(directory)?.java ?? String(c.profile.values.java ?? 'unverified-legacy-executable');
|
|
57
|
+
const directory = c.appArguments.AppDataDirectory, executable = runtimeSelection(directory, analysisDirectory(c))?.java ?? String(c.profile.values.java ?? 'unverified-legacy-executable');
|
|
57
58
|
async function walk(path, depth = 0) {
|
|
58
59
|
if (depth > 6)
|
|
59
60
|
return;
|
|
@@ -54,7 +54,8 @@ export async function initializeModel(workspace, target, binding, server, finger
|
|
|
54
54
|
intent = await initialIntent(workspace, target.id);
|
|
55
55
|
}
|
|
56
56
|
if (!intent || typeof intent.id !== 'string' || !/^[a-f0-9-]{36}$/.test(intent.id) ||
|
|
57
|
-
Object.entries(expected).some(([key, value]) => stable(intent[key]) !== stable(value))
|
|
57
|
+
Object.entries(expected).filter(([key]) => key !== 'input').some(([key, value]) => stable(intent[key]) !== stable(value)) ||
|
|
58
|
+
typeof intent.input !== 'string' || await realpath(intent.input) !== expected.input)
|
|
58
59
|
fail('binding', 'Retained initial model differs from its input, load settings, runtime or repository authority.');
|
|
59
60
|
if (workspace.model(target.id).state === 'opening')
|
|
60
61
|
workspace.beginModelInitialization(target.id);
|
|
@@ -81,7 +81,7 @@ export async function nativeRuntimeFingerprint(ghidraVersion, files) {
|
|
|
81
81
|
}
|
|
82
82
|
export async function nativeInputs(values, directory, c) {
|
|
83
83
|
if (directory)
|
|
84
|
-
values = runtimeValues(dirname(directory), values);
|
|
84
|
+
values = runtimeValues(c?.appArguments.AppDataDirectory ?? dirname(directory), values, typeof values.analysisDirectory === 'string' ? values.analysisDirectory : undefined);
|
|
85
85
|
if (!values.java || !values.ghidraHome)
|
|
86
86
|
fail('configuration', 'Ghidra is not installed. Run insight-cli ghidra install, or supply external runtime paths.');
|
|
87
87
|
const requested = [text(values.java, 4096), text(values.ghidraHome, 4096), text(values.mcpJar ?? packagedMcpJar, 4096)];
|
|
@@ -118,7 +118,7 @@ export async function nativeInputs(values, directory, c) {
|
|
|
118
118
|
const files = await nativeRuntimeFiles(ghidraHome, java);
|
|
119
119
|
const baseFingerprint = await nativeRuntimeFingerprint(ghidraVersion, [...files, { name: 'bridge/GhidraMCP-7.0.0.jar', path: mcpJar }]);
|
|
120
120
|
if (directory) {
|
|
121
|
-
const selected = runtimeSelection(dirname(directory));
|
|
121
|
+
const selected = runtimeSelection(c?.appArguments.AppDataDirectory ?? dirname(directory), typeof values.analysisDirectory === 'string' ? values.analysisDirectory : undefined);
|
|
122
122
|
if (selected && selected.fingerprint !== baseFingerprint)
|
|
123
123
|
fail('binding', 'Selected runtime inputs differ from their installation receipt; reinstall or restore the verified files.');
|
|
124
124
|
}
|
package/dist/src/queries.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { command, Permission } from '@eyeauras/cli-factory';
|
|
3
|
-
import { scope } from './research.js';
|
|
3
|
+
import { scope, number } from './research.js';
|
|
4
4
|
import { fail } from './model.js';
|
|
5
5
|
import { queryResult } from './query-results.js';
|
|
6
6
|
import { key, flag, parsed, option, selectedOptions } from './arguments.js';
|
|
@@ -50,6 +50,8 @@ export function queryCommands(research) {
|
|
|
50
50
|
}
|
|
51
51
|
parameters[p.name] = parsed(p, input);
|
|
52
52
|
}
|
|
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) }) });
|
|
53
55
|
if (nativeQueries.has(q.path)) {
|
|
54
56
|
if (Number(parameters.timeout_seconds ?? 5) > 30)
|
|
55
57
|
fail('invalid', 'Native query timeout must be at most 30 seconds.');
|
|
@@ -62,7 +64,13 @@ export function queryCommands(research) {
|
|
|
62
64
|
await research(c).scripts(who, String(parameters.filter ?? ''), Number(o.limit), Number(o.offset)) :
|
|
63
65
|
await research(c).read(who, q.path, parameters);
|
|
64
66
|
return { ...who, value: value(qualifyQuery(q.path, queryResult(q.path, parameters, value(result)))) };
|
|
65
|
-
}, { permission: Permission.ReadOnly, options: [...selectedOptions, ...q.params.map(option), ...(q.path === '/
|
|
67
|
+
}, { permission: Permission.ReadOnly, options: [...selectedOptions, ...q.params.map(option), ...(q.path === '/get_functions' ? [
|
|
68
|
+
{ flags: '--limit-chars <count>', description: 'Per-function Unicode window, 1–32768', defaultValue: 16000 },
|
|
69
|
+
{ flags: '--offset-chars <count>', description: 'Per-function Unicode offset', defaultValue: 0 },
|
|
70
|
+
{ flags: '--total-chars <count>', description: 'Total returned decompiler characters, 1–128000', defaultValue: 64000 },
|
|
71
|
+
{ flags: '--expect-view <identity>', description: 'Reject continuation from another observed view' },
|
|
72
|
+
{ flags: '--save-owner-directory <path>', description: 'Save full returned native text and manifest in a NEW directory on the owner host' }
|
|
73
|
+
] : []), ...(q.path === '/list_scripts' ?
|
|
66
74
|
[option({ name: 'limit', type: 'integer', required: false, default: '50' }), option({ name: 'offset', type: 'integer', required: false, default: '0' })] : [])] }));
|
|
67
75
|
}
|
|
68
76
|
return [...groups].map(([group, children]) => command(group, `${group} research`, children));
|
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(['/find_dead_code', '/detect_crypto_constants', '/get_valid_data_types',
|
|
3
|
+
export const nativeQueries = new Set(['/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']);
|
package/dist/src/research.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { type FunctionInspectionOptions } from './function-inspection.js';
|
|
1
2
|
import type { Artifact } from './artifacts.js';
|
|
2
3
|
import type { GhidraContext, Scope, ScalarChange, Publication, DebuggerState, GhidraLoadSettings } from './workspace.js';
|
|
3
4
|
import { Workspace } from './workspace.js';
|
|
4
5
|
import type { ToolMode } from './contexts.js';
|
|
5
6
|
import type { ScriptPage } from './scripts.js';
|
|
6
7
|
export interface ResearchBackend {
|
|
8
|
+
observation?(scope: Scope): Promise<unknown>;
|
|
7
9
|
sdkFile?(operation: string, p: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
8
10
|
archivePayloads?(scope: Scope, p: Record<string, unknown>): Promise<unknown>;
|
|
9
11
|
debuggerStart?(scope: Scope): Promise<void>;
|
|
@@ -149,6 +151,25 @@ export declare class Research {
|
|
|
149
151
|
session: string;
|
|
150
152
|
target: string;
|
|
151
153
|
}>;
|
|
154
|
+
inspectFunctions(selected: Scope, parameters: Record<string, unknown>, options: FunctionInspectionOptions): Promise<{
|
|
155
|
+
value: {
|
|
156
|
+
save?: {
|
|
157
|
+
host: string;
|
|
158
|
+
directory: string;
|
|
159
|
+
manifest: string;
|
|
160
|
+
} | undefined;
|
|
161
|
+
view: {
|
|
162
|
+
revision?: string;
|
|
163
|
+
identity: string;
|
|
164
|
+
};
|
|
165
|
+
textBudget: {
|
|
166
|
+
maximumCharacters: number;
|
|
167
|
+
returnedCharacters: number;
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
session: string;
|
|
171
|
+
target: string;
|
|
172
|
+
}>;
|
|
152
173
|
xrefs(selected: Scope, at: string, limit: number, offset: number): Promise<{
|
|
153
174
|
items: ({
|
|
154
175
|
from: null;
|
package/dist/src/research.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { textWindow } from './text-window.js';
|
|
2
|
+
import { functionResult, functionFailure } from './function-results.js';
|
|
3
|
+
import { inspectFunctions } from './function-inspection.js';
|
|
2
4
|
import { fail, id, object, text, digest } from './model.js';
|
|
3
5
|
import { fieldKey, scalarMerge, scalarRefresh } from './merge.js';
|
|
4
6
|
import { verifyMutation } from './mutation-effects.js';
|
|
@@ -45,12 +47,6 @@ function comments(value) {
|
|
|
45
47
|
}
|
|
46
48
|
return observed;
|
|
47
49
|
}
|
|
48
|
-
function functionResult(value, selector) {
|
|
49
|
-
const result = object(value), functions = object(result.functions), found = object(functions[selector]);
|
|
50
|
-
if (Object.keys(functions).length !== 1 || found.error || typeof found.name !== 'string' || !found.name || !found.entry_point)
|
|
51
|
-
fail('partial', 'Function result omitted, failed or changed the requested selector.');
|
|
52
|
-
return found;
|
|
53
|
-
}
|
|
54
50
|
function pageResult(value, key, limit, offset) {
|
|
55
51
|
const result = object(value);
|
|
56
52
|
if (!Array.isArray(result[key]) || result[key].length > limit || !Number.isSafeInteger(result.total) || Number(result.total) < 0 ||
|
|
@@ -286,15 +282,25 @@ export class Research {
|
|
|
286
282
|
const value = await backend.call(selected, '/get_functions', { functions: at, fields: decompile ? 'entry_point,signature,decompiled_code' : 'entry_point,signature', include_call_context: false });
|
|
287
283
|
const result = functionResult(value, at);
|
|
288
284
|
if (typeof result.signature !== 'string')
|
|
289
|
-
|
|
285
|
+
functionFailure(at, 'missing_signature');
|
|
290
286
|
if (decompile && typeof result.decompiled_code !== 'string')
|
|
291
|
-
|
|
287
|
+
functionFailure(at, 'missing_decompiler_text');
|
|
292
288
|
const listing = object(await backend.call(selected, '/get_comment', { address: result.entry_point }));
|
|
293
289
|
const window = decompile ? textWindow(String(result.decompiled_code), page?.limit ?? 16000, page?.offset ?? 0, Boolean(object(result.truncation ?? {}).decompiled_code)) : undefined;
|
|
294
290
|
const { content, ...paging } = window ?? { content: undefined };
|
|
295
291
|
return { ...selected, address: publicAddress(result.entry_point), name: result.name, signature: result.signature, comments: comments(listing), ...(window ? { code: content, ...paging } : {}) };
|
|
296
292
|
});
|
|
297
293
|
}
|
|
294
|
+
async inspectFunctions(selected, parameters, options) {
|
|
295
|
+
return this.ordered(selected, async () => {
|
|
296
|
+
const backend = await this.backend();
|
|
297
|
+
await backend.open(selected);
|
|
298
|
+
const context = this.workspace.context(selected);
|
|
299
|
+
const observation = await backend.observation?.(selected);
|
|
300
|
+
const view = { revision: revision(context.baseRevision), identity: digest({ target: selected.target, context: context.id, generation: context.generation, snapshot: context.workingSnapshot, epoch: context.viewEpoch ?? 0, observation }) };
|
|
301
|
+
return inspectFunctions(backend, selected, parameters, options, view, observation);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
298
304
|
async xrefs(selected, at, limit, offset) {
|
|
299
305
|
return this.ordered(selected, async () => {
|
|
300
306
|
const backend = await this.backend();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runTool } from './tool-runtime/run.js';
|
|
2
|
+
import { analysisDirectory } from './analysis-storage.js';
|
|
2
3
|
import { readFile, mkdir, rename, rm, realpath, readdir, stat, cp } from 'node:fs/promises';
|
|
3
4
|
import { existsSync, realpathSync } from 'node:fs';
|
|
4
5
|
import { join, dirname, resolve, delimiter, basename } from 'node:path';
|
|
@@ -104,9 +105,9 @@ export class RuntimeManager {
|
|
|
104
105
|
}
|
|
105
106
|
// Canonicalize storage once the profile exists, without creating it for passive status.
|
|
106
107
|
// Keep the Factory directory for shared process/lock identity and existing run receipts.
|
|
107
|
-
get root() {
|
|
108
|
+
get root() { const root = runtimeRoot(existsSync(this.directory) ? realpathSync.native(this.directory) : this.directory); return existsSync(root) ? realpathSync.native(root) : root; }
|
|
108
109
|
async status() {
|
|
109
|
-
const selected = runtimeSelection(this.directory), values = selected ? runtimeValuesForStatus(selected, this.c.profile.values) : this.c.profile.values;
|
|
110
|
+
const selected = runtimeSelection(this.directory, analysisDirectory(this.c)), values = selected ? runtimeValuesForStatus(selected, this.c.profile.values) : this.c.profile.values;
|
|
110
111
|
const java = typeof values.java === 'string' ? values.java : undefined, home = typeof values.ghidraHome === 'string' ? values.ghidraHome : undefined;
|
|
111
112
|
const missing = [];
|
|
112
113
|
if (!java || !existsSync(java))
|
|
@@ -203,7 +204,7 @@ export class RuntimeManager {
|
|
|
203
204
|
for (const name of names) {
|
|
204
205
|
if (!/^[a-f0-9-]{36}$/.test(name))
|
|
205
206
|
continue;
|
|
206
|
-
const root = join(this.
|
|
207
|
+
const root = join(analysisDirectory(this.c), 'runtime', parent, name, ...(parent === 'stages' ? ['validation'] : []));
|
|
207
208
|
if (existsSync(join(root, 'server/local.json'))) {
|
|
208
209
|
if (await realpath(root) !== resolve(root))
|
|
209
210
|
fail('binding', 'Retained runtime probe moved outside its owned storage.');
|
|
@@ -285,7 +286,7 @@ export class RuntimeManager {
|
|
|
285
286
|
return withToolLock(this.directory, 'ghidra', async () => {
|
|
286
287
|
await assertUnused(this.directory, 'ghidra');
|
|
287
288
|
await this.cleanupProbes();
|
|
288
|
-
const selected = runtimeSelection(this.directory);
|
|
289
|
+
const selected = runtimeSelection(this.directory, analysisDirectory(this.c));
|
|
289
290
|
if (!selected)
|
|
290
291
|
fail('configuration', 'No managed selection exists; externally configured profile paths are never removed.');
|
|
291
292
|
if (purgeCache && selected.id) {
|
|
@@ -339,7 +340,7 @@ export class RuntimeManager {
|
|
|
339
340
|
}
|
|
340
341
|
async doctor() {
|
|
341
342
|
await this.cleanupProbes();
|
|
342
|
-
const root = join(this.
|
|
343
|
+
const root = join(analysisDirectory(this.c), 'runtime/checks', randomUUID()), values = runtimeValues(this.directory, this.c.profile.values, analysisDirectory(this.c));
|
|
343
344
|
const inputs = await nativeInputs(values, undefined, this.c), native = await this.probe(inputs, root);
|
|
344
345
|
let debuggerCheck = { state: 'host_specific' };
|
|
345
346
|
if (managedGhidraDebugger())
|
|
@@ -9,5 +9,5 @@ export interface RuntimeSelection {
|
|
|
9
9
|
uninstalledAt?: string;
|
|
10
10
|
}
|
|
11
11
|
export declare const runtimeRoot: (directory: string) => string;
|
|
12
|
-
export declare function runtimeSelection(directory: string): RuntimeSelection | undefined;
|
|
13
|
-
export declare function runtimeValues(directory: string, values: Record<string, unknown
|
|
12
|
+
export declare function runtimeSelection(directory: string, analysis?: string): RuntimeSelection | undefined;
|
|
13
|
+
export declare function runtimeValues(directory: string, values: Record<string, unknown>, analysis?: string): Record<string, unknown>;
|