@eyeauras/insight-cli 0.2.3 → 0.3.0

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