@eyeauras/insight-cli 0.2.4 → 0.3.1
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 +9 -1
- package/dist/src/analysis-storage.d.ts +165 -0
- package/dist/src/analysis-storage.js +562 -0
- package/dist/src/cli.js +33 -8
- package/dist/src/contexts.js +8 -8
- package/dist/src/ghidra-processes.js +5 -4
- package/dist/src/initial-model.js +28 -6
- package/dist/src/initial-progress.d.ts +4 -0
- package/dist/src/initial-progress.js +64 -0
- package/dist/src/native-inputs.js +4 -3
- 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/server.js +14 -0
- 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/InsightImportProgress.java +42 -0
- package/references/native/InsightInitialImport.java +8 -2
- package/references/native/InsightSdkFiles.java +13 -10
- package/references/setup.md +31 -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 +9 -1
- package/skill-bundle/insight-cli/references/archive.md +32 -0
- package/skill-bundle/insight-cli/references/managed-runtime.md +162 -0
- package/skill-bundle/insight-cli/references/recovery.md +79 -0
- package/skill-bundle/insight-cli/references/setup.md +98 -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 +27 -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/setup.md +31 -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 +6 -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 +27 -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/setup.md +31 -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 +6 -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
|
@@ -51,9 +51,12 @@ import { artifactsCommands } from './saved-materials.js';
|
|
|
51
51
|
import { RuntimeManager, runtimeCommands, runtimeProcesses } from './runtime-manager.js';
|
|
52
52
|
import { packageVersion } from './package-version.js';
|
|
53
53
|
import { runtimeSelection } from './runtime-selection.js';
|
|
54
|
+
import { AnalysisStorage, analysisDirectory } from './analysis-storage.js';
|
|
55
|
+
import { readInitialProgress } from './initial-progress.js';
|
|
54
56
|
const bindingFields = ['java', 'ghidraHome', 'mcpJar', 'mcpJarSha256', 'server', 'repository', 'readerUser', 'publisherUser', 'maxContexts', 'debuggerPython', 'windbgDir', 'archiveUrl'];
|
|
55
57
|
class Owner {
|
|
56
58
|
options;
|
|
59
|
+
storage;
|
|
57
60
|
profiles = new Map();
|
|
58
61
|
runtimeChanging = new Set();
|
|
59
62
|
async runtime(c, action, quiescent = true) {
|
|
@@ -88,10 +91,11 @@ class Owner {
|
|
|
88
91
|
const directory = c.appArguments.AppDataDirectory;
|
|
89
92
|
if (this.runtimeChanging.has(directory))
|
|
90
93
|
fail('busy', 'Runtime preparation is in progress.');
|
|
91
|
-
const
|
|
94
|
+
const root = analysisDirectory(c);
|
|
95
|
+
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]) });
|
|
92
96
|
let profile = this.profiles.get(directory);
|
|
93
97
|
if (!profile) {
|
|
94
|
-
profile = { workspace: new Workspace(join(
|
|
98
|
+
profile = { workspace: new Workspace(join(root, 'workspace'), c.profile.name), tasks: new Map(), configuration };
|
|
95
99
|
this.profiles.set(directory, profile);
|
|
96
100
|
}
|
|
97
101
|
if (profile.configuration !== configuration)
|
|
@@ -116,6 +120,8 @@ class Owner {
|
|
|
116
120
|
}
|
|
117
121
|
async invalidateProfile(app) {
|
|
118
122
|
const directory = app.AppDataDirectory, profile = this.profiles.get(directory);
|
|
123
|
+
if (this.storage?.busy(directory))
|
|
124
|
+
fail('busy', 'Storage migration is in progress.');
|
|
119
125
|
if (this.runtimeChanging.has(directory))
|
|
120
126
|
fail('busy', 'Runtime preparation is in progress; finish it before changing or deleting the profile.');
|
|
121
127
|
if (toolProfileBusy(directory))
|
|
@@ -124,7 +130,7 @@ class Owner {
|
|
|
124
130
|
fail('busy', 'The profile has active or unverified tool processes.');
|
|
125
131
|
const materials = await readdir(join(directory, 'materials')).catch(error => { if (error.code === 'ENOENT')
|
|
126
132
|
return []; throw error; });
|
|
127
|
-
if (existsSync(join(directory, 'workspace', 'workspace.sqlite')) || materials.length)
|
|
133
|
+
if (existsSync(join(directory, 'storage', 'migration.json')) || existsSync(join(directory, 'workspace', 'workspace.sqlite')) || materials.length)
|
|
128
134
|
fail('blocked', 'Profile has retained research data; preserve its workspace and materials before deleting or reconfiguring it.');
|
|
129
135
|
if (profile) {
|
|
130
136
|
await (await profile.backend)?.dispose();
|
|
@@ -132,6 +138,16 @@ class Owner {
|
|
|
132
138
|
this.profiles.delete(directory);
|
|
133
139
|
}
|
|
134
140
|
}
|
|
141
|
+
async releaseStorage(c) {
|
|
142
|
+
const directory = c.appArguments.AppDataDirectory, profile = this.profiles.get(directory);
|
|
143
|
+
if (!profile)
|
|
144
|
+
return;
|
|
145
|
+
if (profile.tasks.size)
|
|
146
|
+
fail('busy', 'Research commands are active.');
|
|
147
|
+
await (await profile.backend)?.dispose();
|
|
148
|
+
profile.workspace.close();
|
|
149
|
+
this.profiles.delete(directory);
|
|
150
|
+
}
|
|
135
151
|
async dispose() {
|
|
136
152
|
for (const profile of this.profiles.values()) {
|
|
137
153
|
await Promise.allSettled([...profile.tasks.values()]);
|
|
@@ -260,6 +276,8 @@ function scopedFailures(entry, owner, parents = []) {
|
|
|
260
276
|
}
|
|
261
277
|
export function createDefinition(options = {}) {
|
|
262
278
|
const owner = new Owner(options);
|
|
279
|
+
const storage = new AnalysisStorage(options.runtime?.profileStore, c => owner.releaseStorage(c));
|
|
280
|
+
owner.storage = storage;
|
|
263
281
|
const definition = {
|
|
264
282
|
name: 'insight-cli', applicationId: 'insight-cli', version: packageVersion, description: 'Isolated research contexts and coordinated Ghidra analysis',
|
|
265
283
|
resources: [owner], permissions: { categories: [{ name: 'Destructive', description: 'Explicit deletion or replacement of retained research data', enabledByDefault: false }] }, ...(options.runtime ? { runtime: options.runtime } : {}),
|
|
@@ -355,6 +373,11 @@ export function createDefinition(options = {}) {
|
|
|
355
373
|
}, { permission: Permission.ReadOnly, options: [{ flags: '--target <id>', description: 'Inspect one target context' }, ...paged] }),
|
|
356
374
|
]),
|
|
357
375
|
command('ghidra', 'Research through an isolated saved context', [
|
|
376
|
+
command('progress', 'Read initial-model import/publication progress without starting Ghidra', async ({ options: o }, c) => {
|
|
377
|
+
const selected = owner.selected(c), who = scope(o);
|
|
378
|
+
selected.workspace.context(who);
|
|
379
|
+
return { ...who, progress: await readInitialProgress(selected.workspace.directory, who.target) };
|
|
380
|
+
}, { permission: Permission.ReadOnly, options: scoped }),
|
|
358
381
|
command('budgets', 'Inspect or configure native execution budgets', [
|
|
359
382
|
command('show', 'Read effective per-request native budgets', (_invocation, c) => ({ profile: c.profile.name, initialImportSeconds: ghidraBudget(c.profile.values, 'import') / 1000, saveSeconds: ghidraBudget(c.profile.values, 'save') / 1000 }), { permission: Permission.ReadOnly }),
|
|
360
383
|
command('set', 'Configure subsequent native requests without invalidating retained research', async ({ options: o }, c) => {
|
|
@@ -385,7 +408,7 @@ export function createDefinition(options = {}) {
|
|
|
385
408
|
if (!String(error.code).endsWith('.notFound'))
|
|
386
409
|
throw error;
|
|
387
410
|
}
|
|
388
|
-
const settings = { ...previous, autoAnalyze: o.analysis ===
|
|
411
|
+
const settings = { ...previous, autoAnalyze: typeof o.analysis === 'boolean' ? o.analysis : previous?.autoAnalyze ?? false,
|
|
389
412
|
...(o.language ? { language: text(o.language) } : {}), ...(o.compilerSpec ? { compilerSpec: text(o.compilerSpec) } : {}),
|
|
390
413
|
...(o.imageBase ? { imageBase: text(o.imageBase) } : {}), ...(o.loader ? { loader: text(o.loader, 512) } : {}),
|
|
391
414
|
...(o.loaderOptions !== undefined ? { loaderOptions: object(parseDomainJson(text(o.loaderOptions, 16000))) } : {}) };
|
|
@@ -393,7 +416,8 @@ export function createDefinition(options = {}) {
|
|
|
393
416
|
}, { permission: Permission.Update, options: [{ flags: '--session <id>', description: 'Use new for independent work, or an existing session', required: true },
|
|
394
417
|
{ flags: '--target <id>', description: 'Continue existing input instead of supplying a file' }, request,
|
|
395
418
|
{ flags: '--mode <mode>', description: 'Request headless or gui; continuation preserves current mode' },
|
|
396
|
-
{ flags: '--
|
|
419
|
+
{ flags: '--analysis', description: 'Opt in to full initial automatic analysis (new targets only)' },
|
|
420
|
+
{ flags: '--no-analysis', description: 'Skip initial automatic analysis (the default for new targets)' },
|
|
397
421
|
{ flags: '--language <id>', description: 'Explicit initial loader language' },
|
|
398
422
|
{ flags: '--compiler-spec <id>', description: 'Explicit initial compiler specification' },
|
|
399
423
|
{ flags: '--image-base <address>', description: 'Initial image base' },
|
|
@@ -483,8 +507,9 @@ export function createDefinition(options = {}) {
|
|
|
483
507
|
]),
|
|
484
508
|
],
|
|
485
509
|
};
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
510
|
+
const commands = definition.commands?.map(entry => entry.name === 'ghidra' ?
|
|
511
|
+
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; }),
|
|
512
|
+
...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);
|
|
513
|
+
return { ...definition, commands: [...(commands ?? []).map(e => storage.wrap(e)), storage.commands()] };
|
|
489
514
|
}
|
|
490
515
|
export const createInsightCli = (options = {}) => createCli(createDefinition(options));
|
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 {
|
|
@@ -872,8 +872,8 @@ public class ${name} extends GhidraScript {
|
|
|
872
872
|
}
|
|
873
873
|
async closed(scope) {
|
|
874
874
|
const context = this.workspace.context(scope), path = join(this.directory(), 'context', context.id, 'current.json');
|
|
875
|
-
const binding = JSON.parse(await readFile(path, 'utf8')), root =
|
|
876
|
-
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 ||
|
|
877
877
|
stable(JSON.parse(await readFile(join(root, 'process.json'), 'utf8'))) !== stable(binding))
|
|
878
878
|
fail('binding', 'Closed context receipt changed.');
|
|
879
879
|
try {
|
|
@@ -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;
|
|
@@ -2,6 +2,7 @@ import { mkdir, realpath, readFile, open, link, unlink } from 'node:fs/promises'
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { stable, object, fail } from './model.js';
|
|
5
|
+
import { initialProgress, progressPhase, readInitialProgress } from './initial-progress.js';
|
|
5
6
|
export const initialRoot = (workspace, target) => join(workspace.directory, 'ghidra-native', 'initial', target);
|
|
6
7
|
export async function initialIntent(workspace, target) {
|
|
7
8
|
try {
|
|
@@ -15,6 +16,24 @@ export async function initialIntent(workspace, target) {
|
|
|
15
16
|
}
|
|
16
17
|
/** Immutable input/authority binding; private attempts and conditional first creation share it. */
|
|
17
18
|
export async function initializeModel(workspace, target, binding, server, fingerprint) {
|
|
19
|
+
try {
|
|
20
|
+
return await initialize(workspace, target, binding, server, fingerprint);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
try {
|
|
24
|
+
const progress = await readInitialProgress(workspace.directory, target.id);
|
|
25
|
+
// A busy or disconnected caller must not overwrite another live native worker's observation.
|
|
26
|
+
if (progress && progress.state !== 'complete' && (progress.pid === process.pid || progress.workerAlive === false)) {
|
|
27
|
+
const code = String(error.code ?? '');
|
|
28
|
+
const state = code === 'tool.timeout' ? 'timed_out' : code === 'tool.cancelled' ? 'cancelled' : progress.state === 'failed' || progress.state === 'cancelled' ? progress.state : 'unknown';
|
|
29
|
+
await initialProgress(initialRoot(workspace, target.id), target.id, progress.phase, state);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch { /* Preserve the authoritative operation failure when observation storage also fails. */ }
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function initialize(workspace, target, binding, server, fingerprint) {
|
|
18
37
|
const root = initialRoot(workspace, target.id);
|
|
19
38
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
20
39
|
if (await realpath(root) !== resolve(root))
|
|
@@ -54,7 +73,8 @@ export async function initializeModel(workspace, target, binding, server, finger
|
|
|
54
73
|
intent = await initialIntent(workspace, target.id);
|
|
55
74
|
}
|
|
56
75
|
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))
|
|
76
|
+
Object.entries(expected).filter(([key]) => key !== 'input').some(([key, value]) => stable(intent[key]) !== stable(value)) ||
|
|
77
|
+
typeof intent.input !== 'string' || await realpath(intent.input) !== expected.input)
|
|
58
78
|
fail('binding', 'Retained initial model differs from its input, load settings, runtime or repository authority.');
|
|
59
79
|
if (workspace.model(target.id).state === 'opening')
|
|
60
80
|
workspace.beginModelInitialization(target.id);
|
|
@@ -66,20 +86,22 @@ export async function initializeModel(workspace, target, binding, server, finger
|
|
|
66
86
|
const receipt = object(JSON.parse(await readFile(join(root, 'import.json'), 'utf8'))), name = receipt.snapshot;
|
|
67
87
|
if (typeof name !== 'string' || !/^attempt-[a-f0-9-]{36}\.gzf$/.test(name))
|
|
68
88
|
fail('binding', 'Initial model snapshot receipt is malformed.');
|
|
69
|
-
const snapshot = await workspace.captureSnapshot(join(root, name));
|
|
89
|
+
const snapshot = await progressPhase(root, target.id, 'snapshot-verification', () => workspace.captureSnapshot(join(root, name)));
|
|
70
90
|
if (snapshot.sha256 !== receipt.snapshotSha256)
|
|
71
91
|
fail('binding', 'Initial saved snapshot changed before publication.');
|
|
72
|
-
|
|
92
|
+
const publication = (operation) => progressPhase(root, target.id, 'publication-' + operation, () => server.initialPublication(binding, root, operation));
|
|
93
|
+
let result = await publication('stage');
|
|
73
94
|
if (result.state === 'staged') {
|
|
74
|
-
const proof = await
|
|
95
|
+
const proof = await publication('verify-stage');
|
|
75
96
|
if (proof.state !== 'stage_verified' || proof.version !== 1 || proof.snapshotBuffersVerified !== true)
|
|
76
97
|
fail('unknown', 'Independent initial staging readback did not establish the complete snapshot.');
|
|
77
|
-
result = await
|
|
98
|
+
result = await publication('promote');
|
|
78
99
|
}
|
|
79
100
|
if (result.state !== 'published' || result.version !== 1 || result.snapshotBuffersVerified !== true)
|
|
80
101
|
fail('unknown', 'Initial publication requires reconciliation against its saved snapshot.');
|
|
81
|
-
const verified = await
|
|
102
|
+
const verified = await publication('verify');
|
|
82
103
|
if (verified.state !== 'verified' || verified.version !== 1 || verified.snapshotBuffersVerified !== true)
|
|
83
104
|
fail('unknown', 'Independent initial-model readback did not establish the complete saved buffers.');
|
|
84
105
|
workspace.establishModel(target.id, 1, snapshot);
|
|
106
|
+
await initialProgress(root, target.id, 'complete', 'complete');
|
|
85
107
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Observation only. Completion receipts and the workspace remain the outcome authority. */
|
|
2
|
+
export declare function readInitialProgress(directory: string, target: string): Promise<any>;
|
|
3
|
+
export declare function initialProgress(root: string, target: string, phase: string, state?: string): Promise<void>;
|
|
4
|
+
export declare function progressPhase<T>(root: string, target: string, phase: string, action: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { atomicWrite } from './tool-runtime/store.js';
|
|
4
|
+
/** Observation only. Completion receipts and the workspace remain the outcome authority. */
|
|
5
|
+
export async function readInitialProgress(directory, target) {
|
|
6
|
+
try {
|
|
7
|
+
const value = JSON.parse(await readFile(join(directory, 'ghidra-native', 'initial', target, 'progress.json'), 'utf8'));
|
|
8
|
+
if (value.schema !== 1 || value.target !== target)
|
|
9
|
+
throw Error('Invalid initial progress binding');
|
|
10
|
+
const ageMs = Math.max(0, Date.now() - Date.parse(value.updatedAt));
|
|
11
|
+
let workerAlive;
|
|
12
|
+
if (Number.isSafeInteger(value.pid) && value.pid > 0)
|
|
13
|
+
try {
|
|
14
|
+
process.kill(value.pid, 0);
|
|
15
|
+
workerAlive = true;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code === 'ESRCH')
|
|
19
|
+
workerAlive = false;
|
|
20
|
+
}
|
|
21
|
+
return { ...value, operation: 'initial-model', ageMs, elapsedMs: Math.max(0, (value.state === 'running' ? Date.now() : Date.parse(value.updatedAt)) - Date.parse(value.startedAt)),
|
|
22
|
+
...(workerAlive === undefined ? {} : { workerAlive }),
|
|
23
|
+
observation: value.state === 'running' && (workerAlive === false || ageMs > 15000) ? 'stale' : 'current' };
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code === 'ENOENT')
|
|
27
|
+
return null;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function initialProgress(root, target, phase, state = 'running') {
|
|
32
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
33
|
+
let startedAt = new Date().toISOString();
|
|
34
|
+
try {
|
|
35
|
+
const previous = JSON.parse(await readFile(join(root, 'progress.json'), 'utf8'));
|
|
36
|
+
if (previous.target === target)
|
|
37
|
+
startedAt = previous.startedAt;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error.code !== 'ENOENT')
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
await atomicWrite(join(root, 'progress.json'), { schema: 1, target, phase, state, pid: process.pid, startedAt, updatedAt: new Date().toISOString(), indeterminate: true });
|
|
44
|
+
}
|
|
45
|
+
export async function progressPhase(root, target, phase, action) {
|
|
46
|
+
await initialProgress(root, target, phase);
|
|
47
|
+
let pending = Promise.resolve(), failure;
|
|
48
|
+
const timer = setInterval(() => { pending = pending.then(() => initialProgress(root, target, phase)).catch(error => { failure = error; }); }, 2000);
|
|
49
|
+
timer.unref();
|
|
50
|
+
let actionFailed = false;
|
|
51
|
+
try {
|
|
52
|
+
return await action();
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
actionFailed = true;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
clearInterval(timer);
|
|
60
|
+
await pending;
|
|
61
|
+
if (failure && !actionFailed)
|
|
62
|
+
throw failure;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { runTool } from './tool-runtime/run.js';
|
|
2
|
+
import { javaClasspathArguments } from './tool-runtime/java-arguments.js';
|
|
2
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
import { readFile, writeFile, readdir, mkdir, open, rename, rm, realpath, stat } from 'node:fs/promises';
|
|
4
5
|
import { basename, delimiter, dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
@@ -81,7 +82,7 @@ export async function nativeRuntimeFingerprint(ghidraVersion, files) {
|
|
|
81
82
|
}
|
|
82
83
|
export async function nativeInputs(values, directory, c) {
|
|
83
84
|
if (directory)
|
|
84
|
-
values = runtimeValues(dirname(directory), values);
|
|
85
|
+
values = runtimeValues(c?.appArguments.AppDataDirectory ?? dirname(directory), values, typeof values.analysisDirectory === 'string' ? values.analysisDirectory : undefined);
|
|
85
86
|
if (!values.java || !values.ghidraHome)
|
|
86
87
|
fail('configuration', 'Ghidra is not installed. Run insight-cli ghidra install, or supply external runtime paths.');
|
|
87
88
|
const requested = [text(values.java, 4096), text(values.ghidraHome, 4096), text(values.mcpJar ?? packagedMcpJar, 4096)];
|
|
@@ -118,7 +119,7 @@ export async function nativeInputs(values, directory, c) {
|
|
|
118
119
|
const files = await nativeRuntimeFiles(ghidraHome, java);
|
|
119
120
|
const baseFingerprint = await nativeRuntimeFingerprint(ghidraVersion, [...files, { name: 'bridge/GhidraMCP-7.0.0.jar', path: mcpJar }]);
|
|
120
121
|
if (directory) {
|
|
121
|
-
const selected = runtimeSelection(dirname(directory));
|
|
122
|
+
const selected = runtimeSelection(c?.appArguments.AppDataDirectory ?? dirname(directory), typeof values.analysisDirectory === 'string' ? values.analysisDirectory : undefined);
|
|
122
123
|
if (selected && selected.fingerprint !== baseFingerprint)
|
|
123
124
|
fail('binding', 'Selected runtime inputs differ from their installation receipt; reinstall or restore the verified files.');
|
|
124
125
|
}
|
|
@@ -158,7 +159,7 @@ export async function nativeHelpers(directory, inputs, c) {
|
|
|
158
159
|
if (c)
|
|
159
160
|
await runTool(c, { tool: 'ghidra', role: 'compile-helpers', installation: inputs.fingerprint, executable, arguments: args, timeoutMs: 60000, maxOutputBytes: 262144 });
|
|
160
161
|
else
|
|
161
|
-
await run(executable, args, { timeout: 60000, maxBuffer: 262144 });
|
|
162
|
+
await run(executable, await javaClasspathArguments(temporary, 'ghidra', executable, args), { timeout: 60000, maxBuffer: 262144 });
|
|
162
163
|
const complete = await open(join(temporary, 'complete'), 'wx', 0o600);
|
|
163
164
|
try {
|
|
164
165
|
await complete.writeFile(key);
|
|
@@ -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>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
2
2
|
import { join, isAbsolute, resolve } from 'node:path';
|
|
3
|
-
import { fail, object } from './model.js';
|
|
3
|
+
import { fail, object, digest } from './model.js';
|
|
4
4
|
export const runtimeRoot = (directory) => join(directory, 'runtime');
|
|
5
|
-
export function runtimeSelection(directory) {
|
|
5
|
+
export function runtimeSelection(directory, analysis = directory) {
|
|
6
6
|
const path = join(runtimeRoot(directory), 'selection.json');
|
|
7
7
|
if (!existsSync(path))
|
|
8
8
|
return;
|
|
@@ -15,23 +15,30 @@ export function runtimeSelection(directory) {
|
|
|
15
15
|
fail('binding', 'Managed runtime moved outside its owned installation.');
|
|
16
16
|
// Workspace resolves AppData physically; older install receipts can retain a profile
|
|
17
17
|
// alias such as /tmp -> /private/tmp. Accept that same profile, never an inner escape.
|
|
18
|
-
let profile, recorded, home;
|
|
18
|
+
let profile, recorded, owned, home, retained = false;
|
|
19
19
|
try {
|
|
20
20
|
profile = realpathSync.native(directory);
|
|
21
21
|
recorded = realpathSync.native(recordedProfile);
|
|
22
|
+
owned = realpathSync.native(analysis);
|
|
23
|
+
if (recorded !== profile && recorded !== owned) {
|
|
24
|
+
const marker = object(JSON.parse(readFileSync(join(recorded, '.insight-analysis.json'), 'utf8')));
|
|
25
|
+
retained = marker.schema === 1 && marker.binding === digest(profile);
|
|
26
|
+
}
|
|
27
|
+
if (realpathSync.native(runtimeRoot(directory)) !== join(owned, 'runtime') || realpathSync.native(runtimeRoot(recordedProfile)) !== join(owned, 'runtime'))
|
|
28
|
+
fail('binding', 'Managed runtime root differs from its configured analysis owner.');
|
|
22
29
|
if (existsSync(value.ghidraHome))
|
|
23
30
|
home = realpathSync.native(value.ghidraHome);
|
|
24
31
|
}
|
|
25
32
|
catch {
|
|
26
33
|
fail('binding', 'Managed runtime profile ownership could not be verified.');
|
|
27
34
|
}
|
|
28
|
-
if (recorded !== profile || home !== undefined && home !== join(runtimeRoot(
|
|
35
|
+
if (recorded !== profile && recorded !== owned && !retained || home !== undefined && home !== join(runtimeRoot(owned), 'installations', id, 'ghidra'))
|
|
29
36
|
fail('binding', 'Managed runtime moved outside its owned installation.');
|
|
30
37
|
}
|
|
31
38
|
return value;
|
|
32
39
|
}
|
|
33
|
-
export function runtimeValues(directory, values) {
|
|
34
|
-
const selected = runtimeSelection(directory);
|
|
40
|
+
export function runtimeValues(directory, values, analysis) {
|
|
41
|
+
const selected = runtimeSelection(directory, analysis);
|
|
35
42
|
if (!selected)
|
|
36
43
|
return values;
|
|
37
44
|
if (selected.uninstalledAt)
|
|
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { constants } from 'node:fs';
|
|
4
4
|
import { lstat, mkdir, open, opendir, realpath, rename, rm } from 'node:fs/promises';
|
|
5
5
|
import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { analysisDirectory } from './analysis-storage.js';
|
|
6
7
|
const maximumEntries = 100_000, maximumDepth = 128;
|
|
7
8
|
function fail(code, message) { throw new CliError(message, { code: 'artifacts.' + code }); }
|
|
8
9
|
function check(signal) { if (signal?.aborted)
|
|
@@ -190,7 +191,7 @@ export function artifactsCommands() {
|
|
|
190
191
|
command('save <path>', 'Save a complete independent copy of a file or directory in this profile', ({ args }, context) => {
|
|
191
192
|
if (typeof args.path !== 'string' || !args.path || args.path.includes('\0'))
|
|
192
193
|
fail('invalid', 'Provide one file or directory path.');
|
|
193
|
-
return saveMaterials(context
|
|
194
|
+
return saveMaterials(analysisDirectory(context), resolve(context.cwd, args.path), context.signal);
|
|
194
195
|
}, { permission: Permission.Update }),
|
|
195
196
|
])];
|
|
196
197
|
}
|
package/dist/src/server.js
CHANGED
|
@@ -203,6 +203,16 @@ export class GhidraServer {
|
|
|
203
203
|
}
|
|
204
204
|
async initialImport(root) {
|
|
205
205
|
const timeoutMilliseconds = ghidraBudget(this.c.profile.values, 'import');
|
|
206
|
+
const report = async () => {
|
|
207
|
+
try {
|
|
208
|
+
const p = object(JSON.parse(await readFile(join(root, 'progress.json'), 'utf8')));
|
|
209
|
+
const elapsed = Math.round((Date.now() - Date.parse(String(p.startedAt))) / 1000);
|
|
210
|
+
this.c.progress(`Ghidra ${String(p.phase)}: ${elapsed}s${typeof p.total === 'number' ? `; task ${String(p.completed)}/${p.total}` : ''}${p.message ? `; ${String(p.message).replace(/[\r\n\x00-\x1f]/g, ' ')}` : ''}`);
|
|
211
|
+
}
|
|
212
|
+
catch { /* Progress observation never changes the native operation's outcome. */ }
|
|
213
|
+
};
|
|
214
|
+
const timer = setInterval(() => { void report(); }, 5000);
|
|
215
|
+
timer.unref();
|
|
206
216
|
try {
|
|
207
217
|
return await this.run('initial-import', 'InsightInitialImport', [root], {}, undefined, [0], timeoutMilliseconds);
|
|
208
218
|
}
|
|
@@ -211,8 +221,12 @@ export class GhidraServer {
|
|
|
211
221
|
throw new CliError(error.message + ' Initial import remains unproved. Increase profile initialImportSeconds and continue the original scope; incomplete private analysis restarts from the retained input.', { code: error.code, result: { phase: 'initial-import', timeoutMilliseconds, budgetSetting: 'initialImportSeconds', outcome: 'unknown', diagnostics: error.result } });
|
|
212
222
|
throw error;
|
|
213
223
|
}
|
|
224
|
+
finally {
|
|
225
|
+
clearInterval(timer);
|
|
226
|
+
}
|
|
214
227
|
}
|
|
215
228
|
async initialPublication(binding, root, operation = 'stage') {
|
|
229
|
+
this.c.progress('Ghidra initial publication: ' + operation);
|
|
216
230
|
return this.run('initial-publication', 'db.InsightInitialPublication', [root, operation], await this.credentials(binding, operation.startsWith('verify') ? 'reader' : 'publisher'));
|
|
217
231
|
}
|
|
218
232
|
async publish(binding, path, snapshot, expected, request, journal, sourceSha) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { analysisDirectory } from '../analysis-storage.js';
|
|
2
|
+
import { mkdir, stat, rm, readdir, writeFile, readFile, realpath } from 'node:fs/promises';
|
|
2
3
|
import { join, resolve, sep, dirname } from 'node:path';
|
|
3
4
|
import { cacheArtifact, consumeArchive, extractArchive, fileSha256, archiveName } from './archives.js';
|
|
4
5
|
import { atomicWrite, readJson, toolRoot, withToolLock, installationStage, removeInstallation, treeFingerprint, assertUnused, withToolUse } from './store.js';
|
|
@@ -7,7 +8,7 @@ import { digest, fail, text } from '../model.js';
|
|
|
7
8
|
/** Read-only verification used by an owned child after the command owner has staged a runtime. */
|
|
8
9
|
export async function verifyManagedInstallation(directory, tool, selected) {
|
|
9
10
|
const root = join(toolRoot(directory, tool), 'installations', selected.id);
|
|
10
|
-
if (selected.schema !== 1 || selected.tool !== tool || selected.mode !== 'managed' || selected.uninstalledAt || !/^[a-f0-9]{64}$/.test(selected.id) || selected.root !== root || !
|
|
11
|
+
if (selected.schema !== 1 || selected.tool !== tool || selected.mode !== 'managed' || selected.uninstalledAt || !/^[a-f0-9]{64}$/.test(selected.id) || await realpath(selected.root) !== await realpath(root) || !(await realpath(selected.executable)).startsWith(await realpath(root) + sep))
|
|
11
12
|
fail('binding', 'Invalid managed tool installation receipt.');
|
|
12
13
|
const ready = await readJson(join(root, 'ready.json'));
|
|
13
14
|
const binding = (v) => { const { selectedAt, uninstalledAt, ...rest } = v; return rest; };
|
|
@@ -27,8 +28,8 @@ export class ToolInstaller {
|
|
|
27
28
|
this.c = c;
|
|
28
29
|
this.descriptor = descriptor;
|
|
29
30
|
this.directory = c.appArguments.AppDataDirectory;
|
|
30
|
-
this.root = toolRoot(
|
|
31
|
-
this.cache = join(
|
|
31
|
+
this.root = toolRoot(analysisDirectory(c), descriptor.id);
|
|
32
|
+
this.cache = join(analysisDirectory(c), 'runtime/cache');
|
|
32
33
|
}
|
|
33
34
|
async receipt() { return readJson(join(this.root, 'selection.json')); }
|
|
34
35
|
async status() {
|
|
@@ -105,7 +106,7 @@ export class ToolInstaller {
|
|
|
105
106
|
if (selected.schema !== 1 || selected.tool !== this.descriptor.id || !/^[a-f0-9]{64}$/.test(selected.id))
|
|
106
107
|
fail('binding', 'Invalid tool installation receipt.');
|
|
107
108
|
const root = join(this.root, 'installations', selected.id);
|
|
108
|
-
if (selected.root !== root || selected.mode === 'managed' && !
|
|
109
|
+
if (await realpath(selected.root) !== await realpath(root) || selected.mode === 'managed' && !(await realpath(selected.executable)).startsWith(await realpath(root) + sep))
|
|
109
110
|
fail('binding', 'Selected tool points outside its owned installation.');
|
|
110
111
|
const ready = await readJson(join(root, 'ready.json'));
|
|
111
112
|
const binding = (v) => { const { selectedAt, uninstalledAt, ...rest } = v; return rest; };
|
|
@@ -131,7 +132,7 @@ export class ToolInstaller {
|
|
|
131
132
|
await verifyManagedInstallation(this.directory, this.descriptor.id, selected);
|
|
132
133
|
else if (await fileSha256(selected.executable, this.c.signal) !== selected.fingerprint)
|
|
133
134
|
fail('binding', 'Selected tool code differs from its verified installation.');
|
|
134
|
-
if (selected.execution && (!
|
|
135
|
+
if (selected.execution && (!(await realpath(selected.execution.executable)).startsWith(await realpath(root) + sep) || await treeFingerprint(root) !== selected.executionFingerprint))
|
|
135
136
|
fail('binding', 'Owned external-tool execution copy differs from its verified installation.');
|
|
136
137
|
if (selected.mode === 'external' && this.descriptor.externalInputs && (!selected.externalInputs || digest(await this.descriptor.externalInputs(selected.executable, this.c)) !== digest(selected.externalInputs)))
|
|
137
138
|
fail('binding', 'External tool family differs from its verified installation.');
|
|
@@ -19,7 +19,9 @@ async function main() {
|
|
|
19
19
|
if (input.length > 4 * 1024 ** 2)
|
|
20
20
|
throw Error('Launch input exceeds its bound');
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
// Factory's stable profile remains local while the complete runtime tree can be
|
|
23
|
+
// an authorized analysis-storage alias. Child paths must still stay inside it.
|
|
24
|
+
if (await realpath(String(record.root)) !== join(await realpath(join(root, 'runtime')), 'runs', id))
|
|
23
25
|
throw Error('Launch storage changed');
|
|
24
26
|
const plan = JSON.parse(input);
|
|
25
27
|
if (plan.executable !== record.executable)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eyeauras/insight-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-facing durable sessions and isolated Ghidra research contexts",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"prepack": "npm run build"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@eyeauras/cli-factory": "https://raw.githubusercontent.com/iXab3r/insight-cli/
|
|
27
|
+
"@eyeauras/cli-factory": "https://raw.githubusercontent.com/iXab3r/insight-cli/b0d2805ef97face11ab131e8aa39b34216eefaf0/vendor/factory-core-0413409.tgz",
|
|
28
28
|
"@eyeauras/cli-factory-ipc": "https://raw.githubusercontent.com/iXab3r/insight-cli/f47f6d14e8aadc6b6778a6e3627bbb08909521d2/vendor/factory-ipc-7c403d8.tgz",
|
|
29
29
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
30
30
|
"tar": "7.5.22",
|
package/references/artifacts.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Save ordinary research materials
|
|
2
2
|
|
|
3
3
|
`insight-cli artifacts save <path> --json` saves a file or a settled directory tree in
|
|
4
|
-
the selected profile's AppData. It needs Factory Update permission and does not require
|
|
4
|
+
the selected profile's [analysis storage](storage.md), which defaults to AppData. It needs Factory Update permission and does not require
|
|
5
5
|
Ghidra, Java, authentication, target/session selectors or evidence metadata. Relative input
|
|
6
6
|
paths resolve against the caller's working directory. Use `--help` for generated syntax.
|
|
7
7
|
|
|
@@ -79,7 +79,7 @@ operations until `connect`/`start`. `server connect/disconnect` retain their con
|
|
|
79
79
|
An external server remains configured through profile server/repository/account fields and Factory
|
|
80
80
|
secrets; `server stop` cannot stop that external service.
|
|
81
81
|
|
|
82
|
-
Code/cache/installation receipts live under
|
|
82
|
+
Code/cache/installation receipts live under configured [analysis storage](storage.md) `runtime/`; research remains under
|
|
83
83
|
`workspace/`, and saved files under `materials/`. One selected immutable installation serves many
|
|
84
84
|
isolated JVM contexts; each has its own projects, home, settings, cache/temp and authenticated
|
|
85
85
|
endpoint. No per-agent copy of the Ghidra code distribution is required. This is process and data
|