@eyeauras/insight-cli 0.2.4 → 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.
Files changed (91) hide show
  1. package/.cli-package-build.json +1 -1
  2. package/SKILL.md +4 -1
  3. package/dist/src/analysis-storage.d.ts +165 -0
  4. package/dist/src/analysis-storage.js +562 -0
  5. package/dist/src/cli.js +24 -6
  6. package/dist/src/contexts.js +8 -8
  7. package/dist/src/ghidra-processes.js +5 -4
  8. package/dist/src/initial-model.js +2 -1
  9. package/dist/src/native-inputs.js +2 -2
  10. package/dist/src/runtime-manager.js +6 -5
  11. package/dist/src/runtime-selection.d.ts +2 -2
  12. package/dist/src/runtime-selection.js +13 -6
  13. package/dist/src/saved-materials.js +2 -1
  14. package/dist/src/tool-runtime/installer.js +7 -6
  15. package/dist/src/tool-runtime/launch-worker.js +3 -1
  16. package/package.json +2 -2
  17. package/references/artifacts.md +1 -1
  18. package/references/managed-runtime.md +1 -1
  19. package/references/setup.md +4 -0
  20. package/references/storage.md +99 -0
  21. package/runtime/toolbox.json +10 -4
  22. package/skill-bundle/insight-artifacts/SKILL.md +1 -1
  23. package/skill-bundle/insight-artifacts/references/artifacts.md +1 -1
  24. package/skill-bundle/insight-artifacts/references/recovery.md +79 -0
  25. package/skill-bundle/insight-artifacts/references/storage.md +99 -0
  26. package/skill-bundle/insight-binary/SKILL.md +1 -1
  27. package/skill-bundle/insight-capture/SKILL.md +1 -1
  28. package/skill-bundle/insight-cli/SKILL.md +4 -1
  29. package/skill-bundle/insight-cli/references/recovery.md +79 -0
  30. package/skill-bundle/insight-cli/references/storage.md +99 -0
  31. package/skill-bundle/insight-die/SKILL.md +1 -1
  32. package/skill-bundle/insight-dnspy/SKILL.md +1 -1
  33. package/skill-bundle/insight-dotnet-dump/SKILL.md +1 -1
  34. package/skill-bundle/insight-dotnet-stack/SKILL.md +1 -1
  35. package/skill-bundle/insight-dotnet-trace/SKILL.md +1 -1
  36. package/skill-bundle/insight-frida/SKILL.md +1 -1
  37. package/skill-bundle/insight-ghidra/SKILL.md +1 -1
  38. package/skill-bundle/insight-ghidra/references/artifacts.md +1 -1
  39. package/skill-bundle/insight-ghidra/references/managed-runtime.md +1 -1
  40. package/skill-bundle/insight-ghidra/references/setup.md +4 -0
  41. package/skill-bundle/insight-ghidra/references/storage.md +99 -0
  42. package/skill-bundle/insight-ilspy/SKILL.md +1 -1
  43. package/skill-bundle/insight-interop/SKILL.md +1 -1
  44. package/skill-bundle/insight-lldb/SKILL.md +1 -1
  45. package/skill-bundle/insight-llvm/SKILL.md +1 -1
  46. package/skill-bundle/insight-managed-heap/SKILL.md +1 -1
  47. package/skill-bundle/insight-managed-metadata/SKILL.md +1 -1
  48. package/skill-bundle/insight-mitmproxy/SKILL.md +1 -1
  49. package/skill-bundle/insight-overview/SKILL.md +1 -1
  50. package/skill-bundle/insight-procmon/SKILL.md +1 -1
  51. package/skill-bundle/insight-re/SKILL.md +1 -1
  52. package/skill-bundle/insight-re/references/artifacts.md +1 -1
  53. package/skill-bundle/insight-re/references/storage.md +99 -0
  54. package/skill-bundle/insight-sevenzip/SKILL.md +1 -1
  55. package/skill-bundle/insight-strace/SKILL.md +1 -1
  56. package/skill-bundle/insight-tshark/SKILL.md +1 -1
  57. package/skill-bundle/insight-windbg/SKILL.md +1 -1
  58. package/skill-bundle/manifest.json +1 -1
  59. package/skills/insight-artifacts/SKILL.md +1 -1
  60. package/skills/insight-artifacts/references/artifacts.md +1 -1
  61. package/skills/insight-artifacts/references/recovery.md +79 -0
  62. package/skills/insight-artifacts/references/storage.md +99 -0
  63. package/skills/insight-binary/SKILL.md +1 -1
  64. package/skills/insight-capture/SKILL.md +1 -1
  65. package/skills/insight-die/SKILL.md +1 -1
  66. package/skills/insight-dnspy/SKILL.md +1 -1
  67. package/skills/insight-dotnet-dump/SKILL.md +1 -1
  68. package/skills/insight-dotnet-stack/SKILL.md +1 -1
  69. package/skills/insight-dotnet-trace/SKILL.md +1 -1
  70. package/skills/insight-frida/SKILL.md +1 -1
  71. package/skills/insight-ghidra/SKILL.md +1 -1
  72. package/skills/insight-ghidra/references/artifacts.md +1 -1
  73. package/skills/insight-ghidra/references/managed-runtime.md +1 -1
  74. package/skills/insight-ghidra/references/setup.md +4 -0
  75. package/skills/insight-ghidra/references/storage.md +99 -0
  76. package/skills/insight-ilspy/SKILL.md +1 -1
  77. package/skills/insight-interop/SKILL.md +1 -1
  78. package/skills/insight-lldb/SKILL.md +1 -1
  79. package/skills/insight-llvm/SKILL.md +1 -1
  80. package/skills/insight-managed-heap/SKILL.md +1 -1
  81. package/skills/insight-managed-metadata/SKILL.md +1 -1
  82. package/skills/insight-mitmproxy/SKILL.md +1 -1
  83. package/skills/insight-overview/SKILL.md +1 -1
  84. package/skills/insight-procmon/SKILL.md +1 -1
  85. package/skills/insight-re/SKILL.md +1 -1
  86. package/skills/insight-re/references/artifacts.md +1 -1
  87. package/skills/insight-re/references/storage.md +99 -0
  88. package/skills/insight-sevenzip/SKILL.md +1 -1
  89. package/skills/insight-strace/SKILL.md +1 -1
  90. package/skills/insight-tshark/SKILL.md +1 -1
  91. package/skills/insight-windbg/SKILL.md +1 -1
@@ -167,8 +167,8 @@ export class GhidraContexts {
167
167
  }
168
168
  }
169
169
  async verified(binding, target, verifySource = true) {
170
- const root = resolve(binding.root);
171
- if (!root.startsWith(resolve(this.directory()) + sep) || await realpath(root) !== root || binding.target !== target.id || binding.runtimeFingerprint !== this.inputs.fingerprint)
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 = resolve(binding.root);
223
- if (!root.startsWith(resolve(this.directory()) + sep) || await realpath(root) !== root ||
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(binding.root, 'debugger-supervisors'), deadline = Date.now() + 15000;
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 = resolve(binding.root);
876
- if (!root.startsWith(resolve(this.directory()) + sep) || await realpath(root) !== root || binding.context !== context.id || binding.target !== scope.target ||
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(canonical, relative(base, supplied));
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
  }
@@ -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() { return runtimeRoot(existsSync(this.directory) ? realpathSync.native(this.directory) : this.directory); }
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.root, parent, name, ...(parent === 'stages' ? ['validation'] : []));
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.root, 'checks', randomUUID()), values = runtimeValues(this.directory, this.c.profile.values);
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>): 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(profile), 'installations', id, 'ghidra'))
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.appArguments.AppDataDirectory, resolve(context.cwd, args.path), context.signal);
194
+ return saveMaterials(analysisDirectory(context), resolve(context.cwd, args.path), context.signal);
194
195
  }, { permission: Permission.Update }),
195
196
  ])];
196
197
  }
@@ -1,4 +1,5 @@
1
- import { mkdir, stat, rm, readdir, writeFile, readFile } from 'node:fs/promises';
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 || !resolve(selected.executable).startsWith(resolve(root) + sep))
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(this.directory, descriptor.id);
31
- this.cache = join(this.directory, 'runtime/cache');
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' && !resolve(selected.executable).startsWith(resolve(root) + sep))
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 && (!resolve(selected.execution.executable).startsWith(resolve(root) + sep) || await treeFingerprint(root) !== selected.executionFingerprint))
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
- if (await realpath(String(record.root)) !== join(root, 'runtime/runs', id))
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.2.4",
3
+ "version": "0.3.0",
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/73459aecc88da6f878988b64f71ab72b34a83c45/vendor/factory-core-74e7a64.tgz",
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",
@@ -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 profile AppData `runtime/`; research remains 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
@@ -1,5 +1,9 @@
1
1
  # Setup
2
2
 
3
+ Inspect `storage show --json` and read [analysis storage](storage.md) before moving databases.
4
+ Use `storage configure` for an empty profile or `storage migrate` for existing work. Factory
5
+ control metadata and keyring identity stay in their original locations.
6
+
3
7
  Use Node 24+ and `ghidra install` to deliver Ghidra 12.1.2, JDK 21, native components and
4
8
  server support. See [managed runtime](managed-runtime.md) for download/install/offline archives,
5
9
  external executable paths, status/doctor and safe removal. Existing Factory profile `java` and
@@ -0,0 +1,99 @@
1
+ # Analysis storage and migration
2
+
3
+ `storage show --json` reports effective profile storage, availability, active file/byte usage,
4
+ exact included trees and retained migration. The default is Factory profile AppData. No
5
+ checkout, environment or automatic disk fallback changes it. The non-secret setting is
6
+ `analysisDirectory` in Factory's profile (`profile show` also displays it). Use storage commands
7
+ to change it safely.
8
+
9
+ ```sh
10
+ insight-cli permissions grant Update
11
+ insight-cli storage show --json
12
+ insight-cli storage configure --directory "/mnt/analysis volume" --json
13
+ ```
14
+
15
+ `configure` requires no existing analysis entries. With existing data use `migrate`.
16
+ The directory is a **base**: the effective root appends `insight-cli/<profile>`, preserving
17
+ profile isolation. Choose an ordinary empty destination profile directory on a mounted local
18
+ filesystem. Spaces and Unicode are supported. Factory restricts the dedicated destination root to the
19
+ current user, including a Windows ACL; a filesystem that cannot apply this protection fails
20
+ before copying.
21
+
22
+ The complete `workspace`, `materials`, `temp` and `runtime` trees move: immutable inputs,
23
+ artifact/candidate snapshots, workspace SQLite DB/WAL, native private project/context DBs,
24
+ local Ghidra Server repositories, operation/publication/import journals, debugger traces,
25
+ process captures, exports, native helper compilation and verification working data.
26
+ **Runtime installations, acquisition caches, selections and process receipts move too**;
27
+ installation staging and activation must share a filesystem. External tool installations and
28
+ user-selected outputs outside these trees keep their locations.
29
+
30
+ Factory keeps profiles, permissions, IPC election/socket, migration-control metadata and
31
+ application logs in current-user AppData. Secrets stay in the OS keyring. Credential/profile
32
+ identities do not change. Updating/installing npm never migrates a profile automatically.
33
+
34
+ ## Move an existing profile
35
+
36
+ Finish changing commands, close native contexts, and stop this profile's owned server/tool
37
+ processes through ordinary CLI commands. Inspect `tools processes --json`. Unverified lifetimes
38
+ also block migration. Do not kill guessed PIDs or stop other profiles to make preflight pass.
39
+ Factory retains IPC ownership and command admission; other profiles can keep working.
40
+
41
+ ```sh
42
+ insight-cli storage migrate --directory "/mnt/analysis volume" --dry-run --json
43
+ insight-cli storage migrate --directory "/mnt/analysis volume" --json
44
+ insight-cli storage show --json
45
+ ```
46
+
47
+ Dry-run checks separation, destination contents, access, conservative capacity and quiescence
48
+ without reserving/copying a migration. Source/destination/configuration roots cannot be equal
49
+ or nested. Destination symlink roots, escaping source links, special files, unresolved links
50
+ and inventories above one million entries or 128 directory levels are rejected. Internal
51
+ links retain their exact target strings. Preflight does not reserve space; concurrent disk use
52
+ can still cause a copy failure.
53
+
54
+ Migration durably reserves its journal, hashes stable source bytes, streams/syncs the copy,
55
+ independently hashes destination bytes and rechecks the source. Only then does it replace source
56
+ trees with permanent compatibility links and commit the Factory setting. Original trees are
57
+ renamed into `.storage-retained/<migration>` on their original volume. Copy/activation never
58
+ deletes them, including on failures. Existing IDs, requests, journals, candidate snapshots and
59
+ receipts retain exact bytes. Compatibility paths preserve old absolute references: do not remove
60
+ those links or their profile/source ancestors manually after migrating.
61
+
62
+ ## Interruptions, uncertainty and retention
63
+
64
+ ```sh
65
+ insight-cli storage resume --json
66
+ insight-cli storage rollback --json
67
+ ```
68
+
69
+ Ordinary research freezes during incomplete copy/activation/rollback/cleanup. `show` and Factory
70
+ profile inspection remain available. Resume verifies completed files, recopies an incomplete
71
+ file, rechecks capacity and finishes the retained transaction. Source and destination filesystem
72
+ anchor identities are bound durably. Missing/replaced volumes fail without creating a system-disk
73
+ fallback; mount the original volume before resuming.
74
+
75
+ Rollback restores retained source and the prior Factory setting. After activation it requires
76
+ an unchanged destination and a verified retained source. Native reopening or later findings can
77
+ make rollback unsafe, in which case it refuses and preserves both copies. Use forward migration
78
+ to another empty base after using the new location. A rolled-back destination remains for manual
79
+ inspection/removal and is never automatically adopted. Cleanup permanently disables rollback.
80
+
81
+ Unknown script/publication effects do **not** need abandonment before migrating a stopped
82
+ profile. Migration starts no worker, replays no script/check-in and marks no unknown outcome
83
+ applied. Inspect the original request after migrating; existing [recovery](recovery.md)
84
+ restrictions and independent readback requirements still apply.
85
+
86
+ After independent native reopen and saved-file readback, free retained source explicitly:
87
+
88
+ ```sh
89
+ insight-cli permissions grant Destructive
90
+ insight-cli storage cleanup --json
91
+ ```
92
+
93
+ Cleanup verifies retained bytes, records irreversible intent, then deletes only that owned
94
+ backup. Interrupted cleanup continues through `storage resume`; rollback is already disabled.
95
+ Compatibility links, receipts and manifests remain. Clean or roll back the current migration
96
+ before starting another. This release provides no nonempty-destination adoption, reverse
97
+ synchronization, live-DB snapshotting or network-filesystem guarantees. POSIX directory fsync
98
+ is used. Windows has file fsync and rename/junction semantics; Node cannot fsync directories
99
+ there, and internal symlinks can require OS privileges.
@@ -6,7 +6,9 @@
6
6
  "title": "insight-cli",
7
7
  "path": "SKILL.md",
8
8
  "resources": {
9
- "references/agent-execution.md": "references/agent-execution.md"
9
+ "references/agent-execution.md": "references/agent-execution.md",
10
+ "references/storage.md": "references/storage.md",
11
+ "references/recovery.md": "references/recovery.md"
10
12
  }
11
13
  },
12
14
  {
@@ -16,7 +18,8 @@
16
18
  "resources": {
17
19
  "references/recovery.md": "references/recovery.md",
18
20
  "references/artifacts.md": "references/artifacts.md",
19
- "references/agent-execution.md": "references/agent-execution.md"
21
+ "references/agent-execution.md": "references/agent-execution.md",
22
+ "references/storage.md": "references/storage.md"
20
23
  }
21
24
  },
22
25
  {
@@ -35,7 +38,8 @@
35
38
  "references/archive.md": "references/archive.md",
36
39
  "references/extensions-and-sync.md": "references/extensions-and-sync.md",
37
40
  "references/artifacts.md": "references/artifacts.md",
38
- "references/agent-execution.md": "references/agent-execution.md"
41
+ "references/agent-execution.md": "references/agent-execution.md",
42
+ "references/storage.md": "references/storage.md"
39
43
  }
40
44
  },
41
45
  {
@@ -53,7 +57,9 @@
53
57
  "path": "skills/insight-artifacts/SKILL.md",
54
58
  "resources": {
55
59
  "references/artifacts.md": "references/artifacts.md",
56
- "references/agent-execution.md": "references/agent-execution.md"
60
+ "references/agent-execution.md": "references/agent-execution.md",
61
+ "references/storage.md": "references/storage.md",
62
+ "references/recovery.md": "references/recovery.md"
57
63
  }
58
64
  },
59
65
  {
@@ -3,7 +3,7 @@ name: insight-artifacts
3
3
  description: Retain a completed reverse-engineering evidence file or directory as an ordinary checkpoint using insight-cli artifacts save. Use after Ghidra or any external tool; preserve existing project formats and source of truth.
4
4
  ---
5
5
 
6
- # Insight Artifacts 0.2.4
6
+ # Insight Artifacts 0.3.0
7
7
 
8
8
  If local help/status reports AppData or IPC access denied, retry that **read-only probe**
9
9
  with the agent host's normal permission escalation, then use that route for later authorized
@@ -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
 
@@ -0,0 +1,79 @@
1
+ # Continue saved work
2
+
3
+ Retain the session/target returned by initial open. Continue the tuple through
4
+ `insight-cli ghidra open --session <id> --target <id> --json`. Saved private snapshots and
5
+ pending scalar comparisons persist independently of a backend process. Reopening preserves
6
+ private work; it does not silently receive a newer shared version.
7
+
8
+ First-import recovery uses the same scoped `ghidra open` or the original request ID/input.
9
+ Import/analysis writes a private snapshot and completion receipt before any initial publication.
10
+ If its worker is still running, continuation reports busy; wait and continue the same scope.
11
+ If that worker stopped before completing its receipt, continuation can repeat private analysis
12
+ from the immutable input while retaining previous attempts. A completed snapshot is reused.
13
+ Initial publication first creates a uniquely named staging object. A separate read-only process
14
+ verifies its complete saved DB buffers before a conditional server move to the absent canonical
15
+ path. It never checks in a new version of an existing canonical item. Lost promotion replies
16
+ are reconciled against the retained identity, one-version history and complete saved buffers.
17
+ Interrupted staging objects remain retained and can be replaced by another private stage;
18
+ they are not adopted as the canonical model. Foreign or mismatched canonical state stays blocked.
19
+ An older direct create that is provably owned and left only an incomplete `<Recovered>` entry
20
+ can be moved aside without deletion; its bytes and original receipt remain preserved.
21
+
22
+ Legacy first-import failures are also recoverable in the original profile. Continuation first
23
+ establishes that the old owned bootstrap worker has ended, then uses an authoritative repository
24
+ probe. An absent canonical item permits private preparation; an existing first version requires
25
+ independent readback. The old project, intent and original session/target remain preserved.
26
+
27
+ If a response was lost, inspect `sessions status <session> --target <target>`. Never infer
28
+ `applied` from a connection failure. A changing command's optional request ID binds one effective
29
+ intent inside the profile. Repeating the same ID/input retrieves its recorded outcome or
30
+ unresolved state; changing input under that ID fails. Without an ID, calls are new intentions.
31
+
32
+ An unknown or partial effect retains its request intent, snapshots and native stage. Open the
33
+ same tuple to reconcile supported outcomes. A committed merge is verified against its recorded
34
+ server version and complete saved buffers through independent read authority; it is never
35
+ checked in again merely because its response was lost. Until verification succeeds, publication
36
+ for this shared target is blocked across sessions. Saved private work remains retained.
37
+
38
+ Scalar reconciliation observes and saves an already present effect; it does not execute the
39
+ setter again. Supported structural/function edits retain a candidate snapshot after an incomplete
40
+ response; continuation can verify every requested effect in that immutable snapshot and adopt it
41
+ without replay. If the complete result cannot be established, the candidate and previous saved
42
+ private view remain retained. An unproved program/script/external outcome remains unresolved. Preserve the profile
43
+ storage and credential store. Do not delete another session's project, stop a process by guessed
44
+ PID/port, or blindly repeat a script/external action to turn uncertainty into success.
45
+
46
+ The older prototype's `workers`, `findings`, `integrate` and batch `recover` commands are not the
47
+ public facade API. Its coordinator journals remain separate and are never silently migrated.
48
+
49
+ A lost UI navigation response can be verified from the live authenticated cursor without another
50
+ goto. If its original owned GUI process has exited, continuation reports the uncertain UI action
51
+ as disconnected and retains saved program work. It does not recreate the old cursor or re-enable
52
+ a previous prompt policy. Other unproved live UI effects remain retained until their outcome is
53
+ established or the original process is known to have ended.
54
+
55
+ ## Abandon an uncertain private program attempt
56
+
57
+ A post-mutation save timeout does not repeat the script or issue another save in the
58
+ error handler. Status exposes the original request identity and retained phase/budget.
59
+ Arbitrary script effects are never inferred from metadata or from an unverified DB.
60
+ After inspecting the retained attempt, explicitly stop only its verified owned worker
61
+ (`tools processes`, then `tools processes kill <process>` if required). Resume the
62
+ last acknowledged private snapshot with:
63
+
64
+ ```sh
65
+ insight-cli permissions grant Destructive
66
+ insight-cli ghidra abandon --session <session> --target <target> \
67
+ --request-id <original-request> --acknowledge-unknown-effects --json
68
+ insight-cli ghidra open --session <session> --target <target> --json
69
+ ```
70
+
71
+ Abandon requires exactly one unresolved private program operation, an unchanged verified
72
+ previous snapshot/generation, a stopped original worker and no active publication or live
73
+ debugger. It retains the native project, candidate artifacts, original intent/execution and
74
+ previous diagnostic result. Its terminal request is `failed` / `state: abandoned` with
75
+ `outcome: unknown`; no effect is marked applied. Repeating abandonment retrieves the same
76
+ result. Earlier saved private edits remain pending. The original changing request ID cannot
77
+ execute again. Abandoning never undoes external effects or establishes what an arbitrary
78
+ script did; the acknowledgement explicitly accepts that uncertainty. Publication recovery,
79
+ initial import, UI and debugger attempts use their own protocols and cannot use this command.