@eyeauras/insight-cli 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/.cli-package-build.json +1 -1
  2. package/SKILL.md +4 -1
  3. package/dist/src/analysis-default.d.ts +26 -0
  4. package/dist/src/analysis-default.js +149 -0
  5. package/dist/src/analysis-storage.d.ts +39 -20
  6. package/dist/src/analysis-storage.js +112 -16
  7. package/dist/src/arguments.js +9 -4
  8. package/dist/src/cli.js +45 -37
  9. package/dist/src/code-references.d.ts +48 -0
  10. package/dist/src/code-references.js +154 -0
  11. package/dist/src/context-diagnostics.d.ts +25 -0
  12. package/dist/src/context-diagnostics.js +82 -0
  13. package/dist/src/contexts.d.ts +13 -1
  14. package/dist/src/contexts.js +38 -9
  15. package/dist/src/function-boundaries.d.ts +9 -0
  16. package/dist/src/function-boundaries.js +68 -0
  17. package/dist/src/function-entries.d.ts +33 -0
  18. package/dist/src/function-entries.js +84 -0
  19. package/dist/src/function-inspection.d.ts +4 -2
  20. package/dist/src/function-inspection.js +94 -59
  21. package/dist/src/function-results.d.ts +3 -1
  22. package/dist/src/function-results.js +9 -3
  23. package/dist/src/inline-script.d.ts +15 -0
  24. package/dist/src/inline-script.js +62 -0
  25. package/dist/src/internal-timing.d.ts +3 -0
  26. package/dist/src/internal-timing.js +52 -0
  27. package/dist/src/lifecycle.js +14 -4
  28. package/dist/src/native-inputs.d.ts +1 -1
  29. package/dist/src/native-inputs.js +1 -1
  30. package/dist/src/native-queries.js +1 -1
  31. package/dist/src/persistent-client.d.ts +30 -0
  32. package/dist/src/persistent-client.js +125 -0
  33. package/dist/src/queries.js +4 -1
  34. package/dist/src/query-policy.js +9 -1
  35. package/dist/src/research-view.d.ts +6 -0
  36. package/dist/src/research-view.js +9 -0
  37. package/dist/src/research.d.ts +38 -0
  38. package/dist/src/research.js +78 -8
  39. package/dist/src/string-anchors.d.ts +43 -0
  40. package/dist/src/string-anchors.js +176 -0
  41. package/dist/src/sync-policy.js +3 -0
  42. package/dist/src/timed-backend.d.ts +3 -0
  43. package/dist/src/timed-backend.js +16 -0
  44. package/dist/src/transport.d.ts +1 -0
  45. package/dist/src/transport.js +4 -0
  46. package/dist/src/workspace.js +5 -2
  47. package/package.json +12 -1
  48. package/references/native/InsightAnchorScan.java +135 -0
  49. package/references/native/InsightCodeReferences.java +129 -0
  50. package/references/native/InsightFunctionBoundaries.java +56 -0
  51. package/references/native/InsightInlineExecutor.java +143 -0
  52. package/references/native/InsightInlineScript.java +14 -0
  53. package/references/native/InsightPeRanges.java +92 -0
  54. package/references/native/InsightResearchQueries.java +22 -1
  55. package/references/native/InsightResearchView.java +69 -0
  56. package/references/native/InsightStringAnchors.java +49 -0
  57. package/references/persistent-client.mjs +125 -0
  58. package/references/persistent.md +50 -0
  59. package/references/query-semantics.md +12 -0
  60. package/references/recovery.md +36 -0
  61. package/references/research.md +87 -0
  62. package/references/setup.md +1 -1
  63. package/references/sparse-navigation.md +173 -0
  64. package/references/storage.md +35 -7
  65. package/runtime/GhidraMCP-7.0.0.jar +0 -0
  66. package/runtime/GhidraMCP-NOTICE +5 -0
  67. package/runtime/provenance.json +17 -3
  68. package/runtime/research-view.provenance.json +28 -0
  69. package/runtime/toolbox.json +10 -3
  70. package/skill-bundle/insight-artifacts/SKILL.md +1 -1
  71. package/skill-bundle/insight-artifacts/references/recovery.md +36 -0
  72. package/skill-bundle/insight-artifacts/references/storage.md +35 -7
  73. package/skill-bundle/insight-binary/SKILL.md +1 -1
  74. package/skill-bundle/insight-capture/SKILL.md +1 -1
  75. package/skill-bundle/insight-cli/SKILL.md +4 -1
  76. package/skill-bundle/insight-cli/references/persistent-client.mjs +125 -0
  77. package/skill-bundle/insight-cli/references/persistent.md +50 -0
  78. package/skill-bundle/insight-cli/references/recovery.md +36 -0
  79. package/skill-bundle/insight-cli/references/setup.md +1 -1
  80. package/skill-bundle/insight-cli/references/storage.md +35 -7
  81. package/skill-bundle/insight-die/SKILL.md +1 -1
  82. package/skill-bundle/insight-dnspy/SKILL.md +1 -1
  83. package/skill-bundle/insight-dotnet-dump/SKILL.md +1 -1
  84. package/skill-bundle/insight-dotnet-stack/SKILL.md +1 -1
  85. package/skill-bundle/insight-dotnet-trace/SKILL.md +1 -1
  86. package/skill-bundle/insight-frida/SKILL.md +1 -1
  87. package/skill-bundle/insight-ghidra/SKILL.md +14 -1
  88. package/skill-bundle/insight-ghidra/references/persistent-client.mjs +125 -0
  89. package/skill-bundle/insight-ghidra/references/persistent.md +50 -0
  90. package/skill-bundle/insight-ghidra/references/query-semantics.md +12 -0
  91. package/skill-bundle/insight-ghidra/references/recovery.md +36 -0
  92. package/skill-bundle/insight-ghidra/references/research.md +87 -0
  93. package/skill-bundle/insight-ghidra/references/setup.md +1 -1
  94. package/skill-bundle/insight-ghidra/references/sparse-navigation.md +173 -0
  95. package/skill-bundle/insight-ghidra/references/storage.md +35 -7
  96. package/skill-bundle/insight-ilspy/SKILL.md +1 -1
  97. package/skill-bundle/insight-interop/SKILL.md +1 -1
  98. package/skill-bundle/insight-lldb/SKILL.md +1 -1
  99. package/skill-bundle/insight-llvm/SKILL.md +1 -1
  100. package/skill-bundle/insight-managed-heap/SKILL.md +1 -1
  101. package/skill-bundle/insight-managed-metadata/SKILL.md +1 -1
  102. package/skill-bundle/insight-mitmproxy/SKILL.md +1 -1
  103. package/skill-bundle/insight-overview/SKILL.md +1 -1
  104. package/skill-bundle/insight-procmon/SKILL.md +1 -1
  105. package/skill-bundle/insight-re/SKILL.md +4 -1
  106. package/skill-bundle/insight-re/references/persistent-client.mjs +125 -0
  107. package/skill-bundle/insight-re/references/persistent.md +50 -0
  108. package/skill-bundle/insight-re/references/recovery.md +36 -0
  109. package/skill-bundle/insight-re/references/storage.md +35 -7
  110. package/skill-bundle/insight-sevenzip/SKILL.md +1 -1
  111. package/skill-bundle/insight-strace/SKILL.md +1 -1
  112. package/skill-bundle/insight-tshark/SKILL.md +1 -1
  113. package/skill-bundle/insight-windbg/SKILL.md +1 -1
  114. package/skill-bundle/manifest.json +1 -1
  115. package/skills/insight-artifacts/SKILL.md +1 -1
  116. package/skills/insight-artifacts/references/recovery.md +36 -0
  117. package/skills/insight-artifacts/references/storage.md +35 -7
  118. package/skills/insight-binary/SKILL.md +1 -1
  119. package/skills/insight-capture/SKILL.md +1 -1
  120. package/skills/insight-die/SKILL.md +1 -1
  121. package/skills/insight-dnspy/SKILL.md +1 -1
  122. package/skills/insight-dotnet-dump/SKILL.md +1 -1
  123. package/skills/insight-dotnet-stack/SKILL.md +1 -1
  124. package/skills/insight-dotnet-trace/SKILL.md +1 -1
  125. package/skills/insight-frida/SKILL.md +1 -1
  126. package/skills/insight-ghidra/SKILL.md +14 -1
  127. package/skills/insight-ghidra/references/persistent-client.mjs +125 -0
  128. package/skills/insight-ghidra/references/persistent.md +50 -0
  129. package/skills/insight-ghidra/references/query-semantics.md +12 -0
  130. package/skills/insight-ghidra/references/recovery.md +36 -0
  131. package/skills/insight-ghidra/references/research.md +87 -0
  132. package/skills/insight-ghidra/references/setup.md +1 -1
  133. package/skills/insight-ghidra/references/sparse-navigation.md +173 -0
  134. package/skills/insight-ghidra/references/storage.md +35 -7
  135. package/skills/insight-ilspy/SKILL.md +1 -1
  136. package/skills/insight-interop/SKILL.md +1 -1
  137. package/skills/insight-lldb/SKILL.md +1 -1
  138. package/skills/insight-llvm/SKILL.md +1 -1
  139. package/skills/insight-managed-heap/SKILL.md +1 -1
  140. package/skills/insight-managed-metadata/SKILL.md +1 -1
  141. package/skills/insight-mitmproxy/SKILL.md +1 -1
  142. package/skills/insight-overview/SKILL.md +1 -1
  143. package/skills/insight-procmon/SKILL.md +1 -1
  144. package/skills/insight-re/SKILL.md +4 -1
  145. package/skills/insight-re/references/persistent-client.mjs +125 -0
  146. package/skills/insight-re/references/persistent.md +50 -0
  147. package/skills/insight-re/references/recovery.md +36 -0
  148. package/skills/insight-re/references/storage.md +35 -7
  149. package/skills/insight-sevenzip/SKILL.md +1 -1
  150. package/skills/insight-strace/SKILL.md +1 -1
  151. package/skills/insight-tshark/SKILL.md +1 -1
  152. package/skills/insight-windbg/SKILL.md +1 -1
package/dist/src/cli.js CHANGED
@@ -52,6 +52,11 @@ import { RuntimeManager, runtimeCommands, runtimeProcesses } from './runtime-man
52
52
  import { packageVersion } from './package-version.js';
53
53
  import { runtimeSelection } from './runtime-selection.js';
54
54
  import { AnalysisStorage, analysisDirectory } from './analysis-storage.js';
55
+ import { codeReferenceCommands } from './code-references.js';
56
+ import { stringAnchorCommands } from './string-anchors.js';
57
+ import { contextDiagnosticCommands } from './context-diagnostics.js';
58
+ import { traceCommand } from './internal-timing.js';
59
+ import { timedBackend } from './timed-backend.js';
55
60
  import { readInitialProgress } from './initial-progress.js';
56
61
  const bindingFields = ['java', 'ghidraHome', 'mcpJar', 'mcpJarSha256', 'server', 'repository', 'readerUser', 'publisherUser', 'maxContexts', 'debuggerPython', 'windbgDir', 'archiveUrl'];
57
62
  class Owner {
@@ -109,7 +114,7 @@ class Owner {
109
114
  const result = await selected.backend;
110
115
  if (result instanceof GhidraContexts)
111
116
  await managedExtensions(selected.workspace.directory);
112
- return result instanceof GhidraContexts ? result.fork(c) : result;
117
+ return timedBackend(result instanceof GhidraContexts ? result.fork(c) : result);
113
118
  };
114
119
  const research = new Research(profile.workspace, backend, profile.tasks);
115
120
  return { workspace: profile.workspace, research, synchronization: new Synchronization(profile.workspace, research, async () => {
@@ -231,47 +236,50 @@ function scopedFailures(entry, owner, parents = []) {
231
236
  const path = [...parents, entry.name.split(' ')[0]], name = path.join(' ');
232
237
  const run = entry.run;
233
238
  return { ...entry, ...(entry.children ? { children: entry.children.map(child => scopedFailures(child, owner, path)) } : {}), ...(run ? { run: async (input, c) => {
234
- try {
235
- let selected;
239
+ const execute = async () => {
236
240
  try {
237
- selected = scope(input.options);
238
- if (selected.session === 'new')
239
- selected = undefined;
241
+ let selected;
242
+ try {
243
+ selected = scope(input.options);
244
+ if (selected.session === 'new')
245
+ selected = undefined;
246
+ }
247
+ catch { }
248
+ if (!selected)
249
+ return await run(input, c);
250
+ const services = owner.selected(c);
251
+ // First open of an already known target reserves its context inside Research.open.
252
+ // Existing contexts still use the command boundary to order automatic sync phases.
253
+ if (path[1] === 'open')
254
+ try {
255
+ services.workspace.context(selected);
256
+ }
257
+ catch (error) {
258
+ if (error instanceof CliError && error.code === 'coordinator.notFound')
259
+ return await run(input, c);
260
+ throw error;
261
+ }
262
+ return await services.research.boundary(selected, async () => {
263
+ const excluded = ['open', 'merge', 'refresh', 'sync-policy', 'close', 'changes', 'debugger', 'ui', 'version-tracking', 'archive', 'projects', 'repository', 'server', 'formats', 'libraries', 'plugins'];
264
+ const eligible = entry.permission !== Permission.ReadOnly && entry.permission !== undefined && !excluded.includes(path[1]) && !['ghidra types archive-export', 'ghidra documentation transfer'].includes(name);
265
+ return eligible ? services.synchronization.execute(selected, name, { ...input.options, commandArguments: input.args }, async (requestId) => run({ ...input, options: { ...input.options, ...(requestId ? { requestId } : {}) } }, c)) : run(input, c);
266
+ });
240
267
  }
241
- catch { }
242
- if (!selected)
243
- return await run(input, c);
244
- const services = owner.selected(c);
245
- // First open of an already known target reserves its context inside Research.open.
246
- // Existing contexts still use the command boundary to order automatic sync phases.
247
- if (path[1] === 'open')
268
+ catch (error) {
269
+ let selected;
248
270
  try {
249
- services.workspace.context(selected);
271
+ selected = scope(input.options);
250
272
  }
251
- catch (error) {
252
- if (error instanceof CliError && error.code === 'coordinator.notFound')
253
- return await run(input, c);
273
+ catch {
254
274
  throw error;
255
275
  }
256
- return await services.research.boundary(selected, async () => {
257
- const excluded = ['open', 'merge', 'refresh', 'sync-policy', 'close', 'changes', 'debugger', 'ui', 'version-tracking', 'archive', 'projects', 'repository', 'server', 'formats', 'libraries', 'plugins'];
258
- const eligible = entry.permission !== Permission.ReadOnly && entry.permission !== undefined && !excluded.includes(path[1]) && !['ghidra types archive-export', 'ghidra documentation transfer'].includes(name);
259
- return eligible ? services.synchronization.execute(selected, name, { ...input.options, commandArguments: input.args }, async (requestId) => run({ ...input, options: { ...input.options, ...(requestId ? { requestId } : {}) } }, c)) : run(input, c);
260
- });
261
- }
262
- catch (error) {
263
- let selected;
264
- try {
265
- selected = scope(input.options);
266
- }
267
- catch {
268
- throw error;
276
+ if (!(error instanceof CliError))
277
+ throw error;
278
+ const result = error.result && typeof error.result === 'object' && !Array.isArray(error.result) ? error.result : {};
279
+ throw new CliError(error.message, { code: error.code, exitCode: error.exitCode, next: error.next, result: { ...result, ...selected } });
269
280
  }
270
- if (!(error instanceof CliError))
271
- throw error;
272
- const result = error.result && typeof error.result === 'object' && !Array.isArray(error.result) ? error.result : {};
273
- throw new CliError(error.message, { code: error.code, exitCode: error.exitCode, next: error.next, result: { ...result, ...selected } });
274
- }
281
+ };
282
+ return path[1] === 'open' || input.options.session && input.options.target ? traceCommand(join(analysisDirectory(c), 'workspace'), name, execute) : execute();
275
283
  } } : {}) };
276
284
  }
277
285
  export function createDefinition(options = {}) {
@@ -424,7 +432,7 @@ export function createDefinition(options = {}) {
424
432
  { flags: '--loader <name>', description: 'Explicit installed loader name/ID from formats loaders' },
425
433
  { flags: '--loader-options <json>', description: 'Named primitive SDK loader options, requiring --loader' }] }),
426
434
  command('strings', 'Read defined strings', [
427
- command('search', 'Find literal text and usable addresses', ({ options: o }, c) => owner.selected(c).research.strings(scope(o), text(o.contains, 4096), number(o.limit, 100, 1), number(o.offset, 2147483647)), { permission: Permission.ReadOnly, options: [...scoped, { flags: '--contains <text>', description: 'Literal case-insensitive substring', required: true }, ...paged] }),
435
+ command('search', 'Find literal text in defined string data; undefined literals: analysis search-byte-patterns', ({ options: o }, c) => owner.selected(c).research.strings(scope(o), text(o.contains, 4096), number(o.limit, 100, 1), number(o.offset, 2147483647)), { permission: Permission.ReadOnly, options: [...scoped, { flags: '--contains <text>', description: 'Literal case-insensitive substring', required: true }, ...paged] }),
428
436
  ]),
429
437
  command('xrefs', 'Follow cross references', [
430
438
  command('to', 'Find incoming references and their functions', ({ options: o }, c) => owner.selected(c).research.xrefs(scope(o), address(o.address), number(o.limit, 100, 1), number(o.offset, 2147483647)), { permission: Permission.ReadOnly, options: [...scoped, at, ...paged] }),
@@ -508,7 +516,7 @@ export function createDefinition(options = {}) {
508
516
  ],
509
517
  };
510
518
  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; }),
519
+ scopedFailures({ ...entry, children: extendQueries(extendQueries(extendQueries(entry.children ?? [], queryCommands(c => owner.selected(c).research)), mutationCommands(c => owner.selected(c).research)), [...codeReferenceCommands(c => owner.selected(c).research), ...stringAnchorCommands(c => owner.selected(c).research), ...contextDiagnosticCommands(c => owner.selected(c).research, c => owner.selected(c).workspace), ...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
520
  ...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
521
  return { ...definition, commands: [...(commands ?? []).map(e => storage.wrap(e)), storage.commands()] };
514
522
  }
@@ -0,0 +1,48 @@
1
+ import { type CommandContext } from '@eyeauras/cli-factory';
2
+ import { type Research, type ResearchBackend } from './research.js';
3
+ import type { Scope } from './workspace.js';
4
+ export declare const referenceAlgorithm = "x86-64-code-reference-candidates-v1";
5
+ export declare const referenceForms: readonly ["call-rel32", "lea-rip", "mov-rip", "call-rip-slot"];
6
+ type Form = typeof referenceForms[number];
7
+ export interface ReferenceQuery {
8
+ targets: string[];
9
+ forms: Form[];
10
+ start: string;
11
+ end: string;
12
+ }
13
+ export interface ReferenceBudgets {
14
+ max_bytes: number;
15
+ max_candidates: number;
16
+ timeout_seconds: number;
17
+ limit: number;
18
+ }
19
+ export interface ReferenceRequest {
20
+ query: ReferenceQuery;
21
+ from: string;
22
+ budgets: ReferenceBudgets;
23
+ expectedView?: string;
24
+ }
25
+ export declare function referenceRequest(o: Record<string, unknown>, who: Scope): ReferenceRequest;
26
+ /** The deliberately small encoding contract, independently rechecked in the facade. */
27
+ export declare function referenceEncoding(input: Buffer): {
28
+ form: "call-rel32" | "lea-rip" | "mov-rip" | "call-rip-slot";
29
+ length: number;
30
+ displacement: number;
31
+ operand: number;
32
+ bits: number;
33
+ delta: number;
34
+ role: string;
35
+ } | undefined;
36
+ export declare function referenceResult(raw: unknown, r: ReferenceRequest): Record<string, unknown>;
37
+ export declare function inspectCodeReferences(backend: ResearchBackend, who: Scope, r: ReferenceRequest, view: string, observation: unknown): Promise<{
38
+ value: {
39
+ view: {
40
+ identity: string;
41
+ };
42
+ cursor: string | null;
43
+ };
44
+ session: string;
45
+ target: string;
46
+ }>;
47
+ export declare function codeReferenceCommands(research: (c: CommandContext) => Research): import("@eyeauras/cli-factory").CommandDefinition[];
48
+ export {};
@@ -0,0 +1,154 @@
1
+ import { command, Permission } from '@eyeauras/cli-factory';
2
+ import { scope, number, address } from './research.js';
3
+ import { fail, object, digest, stable } from './model.js';
4
+ import { parseDomainJson } from './json.js';
5
+ import { selectedOptions } from './arguments.js';
6
+ export const referenceAlgorithm = 'x86-64-code-reference-candidates-v1';
7
+ export const referenceForms = ['call-rel32', 'lea-rip', 'mov-rip', 'call-rip-slot'];
8
+ const defaults = { max_bytes: 4194304, max_candidates: 200000, timeout_seconds: 5, limit: 100 };
9
+ const hex = (v) => { const s = address(v); if (!/^[a-f0-9]{1,16}$/.test(s))
10
+ fail('invalid', 'Code-reference V1 requires default-space unsigned 64-bit addresses.'); return s; };
11
+ const big = (s) => BigInt('0x' + s);
12
+ function query(v) {
13
+ const q = object(v);
14
+ if (!Array.isArray(q.targets) || q.targets.length < 1 || q.targets.length > 64 || !Array.isArray(q.forms) || !q.forms.length || q.forms.length > 4)
15
+ fail('invalid', 'Select 1–64 targets and 1–4 reference forms.');
16
+ const targets = q.targets.map(hex).sort((a, b) => big(a) < big(b) ? -1 : big(a) > big(b) ? 1 : 0), forms = referenceForms.filter(f => q.forms.includes(f));
17
+ if (new Set(targets).size !== targets.length || forms.length !== q.forms.length)
18
+ fail('invalid', 'Targets/forms must be distinct supported values.');
19
+ const start = hex(q.start), end = hex(q.end);
20
+ if (big(start) > big(end))
21
+ fail('invalid', 'scan-through must not precede scan-from.');
22
+ return { targets, forms, start, end };
23
+ }
24
+ function budgets(v, base = defaults) { return { max_bytes: number(v.maxBytes ?? base.max_bytes, 16777216, 15), max_candidates: number(v.maxCandidates ?? base.max_candidates, 1000000, 1), timeout_seconds: number(v.timeoutSeconds ?? base.timeout_seconds, 30, 1), limit: number(v.limit ?? base.limit, 1000, 1) }; }
25
+ export function referenceRequest(o, who) {
26
+ if (o.cursor !== undefined) {
27
+ if (['to', 'forms', 'scanFrom', 'scanThrough'].some(k => o[k] !== undefined))
28
+ fail('invalid', 'Cursor continuation cannot change targets, forms or scan bounds.');
29
+ if (typeof o.cursor !== 'string' || o.cursor.length > 16384 || !/^cr1\.[A-Za-z0-9_-]+$/.test(o.cursor))
30
+ fail('invalid', 'Malformed code-reference cursor.');
31
+ let c;
32
+ try {
33
+ const encoded = o.cursor.slice(4), bytes = Buffer.from(encoded, 'base64url');
34
+ if (bytes.toString('base64url') !== encoded)
35
+ throw Error();
36
+ c = object(parseDomainJson(bytes.toString('utf8')));
37
+ }
38
+ catch {
39
+ fail('invalid', 'Malformed code-reference cursor.');
40
+ }
41
+ if (c.version !== 1 || c.algorithm !== referenceAlgorithm || stable(c.scope) !== stable(who) || typeof c.view !== 'string' || !/^[a-f0-9]{64}$/.test(c.view))
42
+ fail('invalid', 'Cursor version, scope or view is incompatible.');
43
+ const q = query(c.query), from = hex(c.next), b = object(c.budgets);
44
+ if (big(from) < big(q.start) || big(from) > big(q.end))
45
+ fail('invalid', 'Cursor frontier is outside its range.');
46
+ if (['max_bytes', 'max_candidates', 'timeout_seconds', 'limit'].some(k => !Number.isSafeInteger(b[k])))
47
+ fail('invalid', 'Malformed cursor budgets.');
48
+ const inherited = budgets({ maxBytes: b.max_bytes, maxCandidates: b.max_candidates, timeoutSeconds: b.timeout_seconds, limit: b.limit });
49
+ return { query: q, from, budgets: budgets(o, inherited), expectedView: c.view };
50
+ }
51
+ if (typeof o.to !== 'string' || typeof o.scanFrom !== 'string' || typeof o.scanThrough !== 'string')
52
+ fail('invalid', 'Supply --to, --scan-from and --scan-through, or --cursor.');
53
+ const q = query({ targets: o.to.split(',').map(x => x.trim()), forms: o.forms === undefined ? [...referenceForms] : String(o.forms).split(',').map(x => x.trim()), start: o.scanFrom, end: o.scanThrough });
54
+ return { query: q, from: q.start, budgets: budgets(o) };
55
+ }
56
+ /** The deliberately small encoding contract, independently rechecked in the facade. */
57
+ export function referenceEncoding(input) {
58
+ let p = 0, rex = 0, word = false;
59
+ while (p < input.length && p < 15) {
60
+ const b = input[p];
61
+ if ([0x66, 0x26, 0x2e, 0x36, 0x3e].includes(b)) {
62
+ word ||= b === 0x66;
63
+ rex = 0;
64
+ p++;
65
+ }
66
+ else if (b >= 0x40 && b <= 0x4f) {
67
+ rex = b;
68
+ p++;
69
+ }
70
+ else
71
+ break;
72
+ }
73
+ const op = input[p], m = input[p + 1];
74
+ let form, displacement, length, operand, bits;
75
+ if (op === 0xe8) {
76
+ form = 'call-rel32';
77
+ length = p + 5;
78
+ displacement = p + 1;
79
+ operand = 0;
80
+ bits = 64;
81
+ }
82
+ else {
83
+ if (m === undefined || (m & 0xc7) !== 5)
84
+ return;
85
+ if (op === 0x8d) {
86
+ form = 'lea-rip';
87
+ operand = 1;
88
+ }
89
+ else if (op !== undefined && [0x88, 0x89, 0x8a, 0x8b].includes(op)) {
90
+ form = 'mov-rip';
91
+ operand = op === 0x88 || op === 0x89 ? 0 : 1;
92
+ }
93
+ else if (op === 0xff && ((m >> 3) & 7) === 2) {
94
+ form = 'call-rip-slot';
95
+ operand = 0;
96
+ }
97
+ else
98
+ return;
99
+ length = p + 6;
100
+ displacement = p + 2;
101
+ bits = form === 'call-rip-slot' ? 64 : op === 0x88 || op === 0x8a ? 8 : rex & 8 ? 64 : word ? 16 : 32;
102
+ }
103
+ if (length > 15 || input.length < length)
104
+ return;
105
+ return { form, length, displacement, operand, bits, delta: input.readInt32LE(displacement), role: form === 'call-rel32' ? 'direct-call-target' : form === 'call-rip-slot' ? 'pointer-slot' : 'effective-address' };
106
+ }
107
+ export function referenceResult(raw, r) {
108
+ const v = object(raw), s = object(v.scan), q = r.query, b = r.budgets;
109
+ if (v.algorithm !== referenceAlgorithm || typeof v.language !== 'string' || !v.language.startsWith('x86:LE:64:') || stable(v.forms) !== stable(q.forms) || !Array.isArray(v.matches) || v.matches.length > b.limit || object(v.read_only ?? {}).modification_unchanged !== true)
110
+ fail('partial', 'Malformed code-reference result.');
111
+ for (const k of ['bytes_read', 'tested_starts', 'visited_candidates'])
112
+ if (!Number.isSafeInteger(s[k]) || Number(s[k]) < 0)
113
+ fail('partial', 'Malformed reference scan counters.');
114
+ const complete = s.complete;
115
+ if (typeof complete !== 'boolean' || Number(s.bytes_read) > b.max_bytes || Number(s.tested_starts) > Number(s.bytes_read) || Number(s.visited_candidates) > b.max_candidates || Number(s.visited_candidates) < v.matches.length || Number(s.visited_candidates) > Number(s.tested_starts) || !['exhausted', 'byte_limit', 'candidate_limit', 'timeout', 'result_limit'].includes(String(s.reason)) || complete !== (s.reason === 'exhausted') || !Number.isFinite(s.elapsed_ms) || Number(s.elapsed_ms) < 0 || hex(s.from) !== r.from || hex(s.through) !== q.end)
116
+ fail('partial', 'Inconsistent code-reference coverage.');
117
+ const next = complete ? null : hex(s.next_start);
118
+ if (complete && s.next_start !== null || next !== null && (big(next) < big(r.from) || big(next) > big(q.end)) || next !== null && BigInt(Number(s.tested_starts)) > big(next) - big(r.from))
119
+ fail('partial', 'Reference frontier changed.');
120
+ let previous = -1n;
121
+ for (const rawRow of v.matches) {
122
+ const row = object(rawRow), from = hex(row.from), to = hex(row.to), bytes = row.bytes;
123
+ if (typeof bytes !== 'string' || !/^(?:[a-f0-9]{2}){1,15}$/.test(bytes))
124
+ fail('partial', 'Reference omitted its instruction bytes.');
125
+ const encoding = referenceEncoding(Buffer.from(bytes, 'hex'));
126
+ if (!encoding || row.form !== encoding.form || !q.forms.includes(encoding.form) || row.length !== encoding.length || bytes.length !== encoding.length * 2 || row.operand_index !== encoding.operand || row.operand_bits !== encoding.bits || row.target_role !== encoding.role || !q.targets.includes(to) || big(to) !== BigInt.asUintN(64, big(from) + BigInt(encoding.length + encoding.delta)) || big(from) <= previous || big(from) < big(r.from) || big(from) + BigInt(encoding.length - 1) > big(q.end) || next !== null && big(from) >= big(next) || !['listing-confirmed', 'listing-conflict', 'unconfirmed'].includes(String(row.boundary)) || row.validation !== 'decoded' || typeof row.instruction !== 'string' || row.instruction.length > 512)
127
+ fail('partial', 'Reference bytes, target, form or frontier are inconsistent.');
128
+ previous = big(from);
129
+ }
130
+ if (Buffer.byteLength(JSON.stringify(v)) > 1048576)
131
+ fail('limit', 'Code-reference response exceeds one MiB.');
132
+ return v;
133
+ }
134
+ export async function inspectCodeReferences(backend, who, r, view, observation) {
135
+ if (r.expectedView !== undefined && r.expectedView !== view)
136
+ fail('binding', 'Code-reference continuation view changed.', { reason: 'changed_view' });
137
+ const q = r.query, result = await backend.call(who, '/code_references', { targets: q.targets, forms: q.forms, scan_from: r.from, scan_through: q.end, ...r.budgets });
138
+ if (digest(observation) !== digest(await backend.observation(who)))
139
+ fail('binding', 'Native view changed during code-reference scan.', { reason: 'changed_view' });
140
+ const value = referenceResult(result, r), s = object(value.scan);
141
+ const cursor = s.complete ? null : 'cr1.' + Buffer.from(stable({ version: 1, algorithm: referenceAlgorithm, scope: who, query: q, next: hex(s.next_start), view, budgets: r.budgets })).toString('base64url');
142
+ return { ...who, value: { ...value, view: { identity: view }, cursor } };
143
+ }
144
+ export function codeReferenceCommands(research) {
145
+ return [command('analysis', 'Static analysis', [command('code-references', 'Find bounded x86-64 byte reference candidates without changing Listing or xrefs', ({ options: o }, c) => { const who = scope(o); return research(c).codeReferences(who, referenceRequest(o, who)); }, { permission: Permission.ReadOnly, options: [...selectedOptions,
146
+ { flags: '--to <addresses>', description: '1–64 distinct exact default-space targets, comma separated' },
147
+ { flags: '--forms <forms>', description: 'call-rel32,lea-rip,mov-rip,call-rip-slot; defaults to all four' },
148
+ { flags: '--scan-from <address>', description: 'First candidate start (initial request)' }, { flags: '--scan-through <address>', description: 'Inclusive final byte; matches must fit completely' },
149
+ { flags: '--cursor <token>', description: 'Stateless continuation; excludes targets/forms/range selectors' },
150
+ { flags: '--max-bytes <count>', description: '15–16777216 bytes read including lookahead; default 4194304' },
151
+ { flags: '--max-candidates <count>', description: '1–1000000 prefiltered starts including rejected candidates; default 200000' },
152
+ { flags: '--timeout-seconds <count>', description: '1–30 second cooperative native scan deadline; default 5' },
153
+ { flags: '--limit <count>', description: '1–1000 matches per call; default 100' }] })])];
154
+ }
@@ -0,0 +1,25 @@
1
+ import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
2
+ import { type Research } from './research.js';
3
+ import type { Workspace, Scope } from './workspace.js';
4
+ export type StopReason = 'explicit_close' | 'view_replaced' | 'mode_changed' | 'owner_dispose' | 'startup_failed' | 'temporary_complete' | 'legacy_cleanup' | 'unknown';
5
+ type Event = {
6
+ at: string;
7
+ event: 'start' | 'restore' | 'stop';
8
+ reason: string;
9
+ pid: number;
10
+ };
11
+ export declare function recordLifecycle(workspace: Workspace, context: string, event: Event): Promise<void>;
12
+ export declare function lifecycleDiagnostic(workspace: Workspace, selected: Scope): Promise<{
13
+ lifecycle: {
14
+ ownerShutdownCause: string;
15
+ coverage: string;
16
+ latest?: Record<string, unknown> | undefined;
17
+ events: Event[];
18
+ maximumEvents: number;
19
+ };
20
+ session: string;
21
+ target: string;
22
+ }>;
23
+ export declare function navigationResult(raw: unknown, maximum: number): Record<string, unknown>;
24
+ export declare function contextDiagnosticCommands(research: (c: CommandContext) => Research, workspace: (c: CommandContext) => Workspace): CommandDefinition[];
25
+ export {};
@@ -0,0 +1,82 @@
1
+ import { command, Permission } from '@eyeauras/cli-factory';
2
+ import { mkdir, readFile, writeFile, rename, realpath } from 'node:fs/promises';
3
+ import { join, resolve, sep } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { scope, number } from './research.js';
6
+ import { object, fail, stable } from './model.js';
7
+ import { selectedOptions } from './arguments.js';
8
+ const writes = new Map();
9
+ const journal = (workspace, context) => join(workspace.directory, 'diagnostics', 'lifecycle', context + '.json');
10
+ const reasons = new Set(['explicit_close', 'view_replaced', 'mode_changed', 'owner_dispose', 'startup_failed', 'temporary_complete', 'legacy_cleanup', 'unknown', 'native_launch', 'saved_snapshot']);
11
+ async function events(path) {
12
+ try {
13
+ const rows = JSON.parse(await readFile(path, 'utf8'));
14
+ if (!Array.isArray(rows) || rows.length > 128 || rows.some(row => !row || typeof row.at !== 'string' || row.at.length > 64 || !Number.isFinite(Date.parse(row.at)) || !['start', 'restore', 'stop'].includes(row.event) || !reasons.has(row.reason) || !Number.isSafeInteger(row.pid) || row.pid < 1))
15
+ fail('binding', 'Retained lifecycle observations are malformed.');
16
+ return rows.map(({ at, event, reason, pid }) => ({ at, event, reason, pid }));
17
+ }
18
+ catch (error) {
19
+ if (error.code === 'ENOENT')
20
+ return [];
21
+ throw error;
22
+ }
23
+ }
24
+ export async function recordLifecycle(workspace, context, event) {
25
+ const path = journal(workspace, context), previous = writes.get(path) ?? Promise.resolve();
26
+ const pending = previous.catch(() => { }).then(async () => { const rows = [...await events(path), event].slice(-128), directory = join(workspace.directory, 'diagnostics', 'lifecycle'); await mkdir(directory, { recursive: true, mode: 0o700 }); const temporary = path + '.new-' + randomUUID(); await writeFile(temporary, JSON.stringify(rows) + '\n', { mode: 0o600, flag: 'wx' }); await rename(temporary, path); });
27
+ writes.set(path, pending);
28
+ try {
29
+ await pending;
30
+ }
31
+ catch { }
32
+ finally {
33
+ if (writes.get(path) === pending)
34
+ writes.delete(path);
35
+ }
36
+ }
37
+ export async function lifecycleDiagnostic(workspace, selected) {
38
+ const context = workspace.context(selected), root = join(workspace.directory, 'ghidra-native', 'context', context.id);
39
+ let latest;
40
+ try {
41
+ const binding = object(JSON.parse(await readFile(join(root, 'current.json'), 'utf8')));
42
+ if (binding.context !== context.id || binding.target !== selected.target || binding.session !== undefined && binding.session !== selected.session || typeof binding.root !== 'string' || !Number.isSafeInteger(binding.pid) || typeof binding.tokenSecret !== 'string')
43
+ fail('binding', 'Lifecycle receipt belongs to another context.');
44
+ const physical = await realpath(binding.root);
45
+ if (physical !== resolve(binding.root) || !physical.startsWith(resolve(root) + sep))
46
+ fail('binding', 'Lifecycle receipt moved outside its context.');
47
+ const persisted = object(JSON.parse(await readFile(join(physical, 'process.json'), 'utf8')));
48
+ if (stable(persisted) !== stable(binding))
49
+ fail('binding', 'Lifecycle process receipt changed.');
50
+ latest = { pid: binding.pid, state: 'unobserved', reason: 'unknown' };
51
+ try {
52
+ const stopped = object(JSON.parse(await readFile(join(physical, 'stopped.json'), 'utf8')));
53
+ if (stopped.pid === binding.pid && stopped.tokenSecret === binding.tokenSecret)
54
+ latest = { pid: binding.pid, state: 'stopped', reason: reasons.has(String(stopped.reason)) ? stopped.reason : 'unknown', ...(typeof stopped.at === 'string' ? { at: stopped.at } : {}) };
55
+ }
56
+ catch (error) {
57
+ if (error.code !== 'ENOENT')
58
+ throw error;
59
+ }
60
+ }
61
+ catch (error) {
62
+ if (error.code !== 'ENOENT')
63
+ throw error;
64
+ }
65
+ return { ...selected, lifecycle: { events: await events(journal(workspace, context.id)), maximumEvents: 128, ...(latest ? { latest } : {}), ownerShutdownCause: 'unknown', coverage: 'Retained facade observations, at most the latest 128 events; absent events and historical causes remain unknown. This read neither starts nor probes a native process.' } };
66
+ }
67
+ export function navigationResult(raw, maximum) {
68
+ const value = object(raw), scan = object(value.scan), functions = object(value.functions);
69
+ const samples = ['functions', 'instructions', 'references', 'definedData'].map(key => object(value[key]));
70
+ if (samples.some(row => !Number.isSafeInteger(row.sampled) || Number(row.sampled) < 0 || Number(row.sampled) > 128 || typeof row.complete !== 'boolean') ||
71
+ !Number.isSafeInteger(scan.visited) || Number(scan.visited) !== samples.reduce((n, row) => n + Number(row.sampled), 0) || Number(scan.visited) > maximum || scan.maximum !== maximum || scan.sampleLimitPerCategory !== 128 || typeof scan.complete !== 'boolean' || scan.complete !== samples.every(row => row.complete) || !['budget', 'sample_limit', 'exhausted'].includes(String(scan.reason)) ||
72
+ !Number.isSafeInteger(functions.oneByteBodies) || Number(functions.oneByteBodies) < 0 || Number(functions.oneByteBodies) > Number(functions.sampled) || !Number.isSafeInteger(functions.entriesWithInstructions) || Number(functions.entriesWithInstructions) < 0 || Number(functions.entriesWithInstructions) > Number(functions.sampled) || !Number.isSafeInteger(functions.bodyBytes) || Number(functions.bodyBytes) < 0 ||
73
+ !Number.isSafeInteger(samples[3].strings) || Number(samples[3].strings) < 0 || Number(samples[3].strings) > Number(samples[3].sampled) || !['sparse-metadata', 'no-code-observed', 'observed-metadata'].includes(String(value.assessment)) || !Array.isArray(value.recommendations) || value.recommendations.some(v => typeof v !== 'string') || object(value.read_only ?? {}).modification_unchanged !== true)
74
+ fail('partial', 'Malformed navigation diagnostic.');
75
+ return value;
76
+ }
77
+ export function contextDiagnosticCommands(research, workspace) {
78
+ return [
79
+ command('analysis', 'Analysis observations', [command('navigation', 'Inspect bounded navigation metadata; ready is not analysis completeness', ({ options: o }, c) => research(c).navigation(scope(o), number(o.maxScan ?? 10000, 200000, 1)), { permission: Permission.ReadOnly, options: [...selectedOptions, { flags: '--max-scan <count>', description: 'Maximum visited metadata inputs across all samples, 1–200000', defaultValue: 10000 }] })]),
80
+ command('diagnostics', 'Inspect retained context diagnostics', [command('lifecycle', 'Read retained start/restore/stop reasons without starting Ghidra', ({ options: o }, c) => lifecycleDiagnostic(workspace(c), scope(o)), { permission: Permission.ReadOnly, options: selectedOptions })]),
81
+ ];
82
+ }
@@ -62,7 +62,19 @@ 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
+ observation(scope: Scope): Promise<{
66
+ incarnation: string;
67
+ }>;
68
+ inspectObservation(scope: Scope): Promise<{
69
+ incarnation: string;
70
+ modification: string;
71
+ language: unknown;
72
+ compiler: unknown;
73
+ }>;
74
+ inlineScript(scope: Scope, parameters: Record<string, unknown>, requestId: string): Promise<{
75
+ response: Record<string, unknown>;
76
+ safe: boolean;
77
+ }>;
66
78
  call(scope: Scope, operation: string, parameters?: Record<string, unknown>): Promise<unknown>;
67
79
  saved(scope: Scope): Promise<Artifact>;
68
80
  scripts(scope: Scope, filter: string, limit: number, offset: number): Promise<import("./scripts.js").ScriptPage>;
@@ -1,3 +1,4 @@
1
+ import { recordLifecycle } from './context-diagnostics.js';
1
2
  import { ghidraBudget } from './budgets.js';
2
3
  import { launchProcess, terminateProcess } from './tool-runtime/supervisor.js';
3
4
  import { runTool } from './tool-runtime/run.js';
@@ -11,7 +12,7 @@ import { createServer } from 'node:net';
11
12
  import { setTimeout as delay } from 'node:timers/promises';
12
13
  import { nativeHelpers, nativeInputs, nativeJvmDirectories, nativeExtensionArguments } from './native-inputs.js';
13
14
  import { GhidraServer } from './server.js';
14
- import { object, fail, text, version, stable } from './model.js';
15
+ import { object, fail, text, version, stable, digest } from './model.js';
15
16
  import { HttpBridge } from './transport.js';
16
17
  import { scriptInventory, scriptPath } from './scripts.js';
17
18
  import { logicalPrograms } from './cross-results.js';
@@ -21,6 +22,8 @@ import { readCallerGoal, awaitCaller } from './debugger-caller.js';
21
22
  import { debuggerProgramMetadata } from './debugger-results.js';
22
23
  import { nativeQueries } from './query-policy.js';
23
24
  import { nativeQuery } from './native-queries.js';
25
+ import { runInline } from './inline-script.js';
26
+ import { researchView } from './research-view.js';
24
27
  import { sdkWrites } from './sdk-contracts.js';
25
28
  import { initialIntent, initializeModel } from './initial-model.js';
26
29
  import { managedGhidraDebugger, prepareGhidraDebugger } from './ghidra-debugger-runtime.js';
@@ -149,6 +152,8 @@ export class GhidraContexts {
149
152
  const info = await record.http.instance();
150
153
  if (info.pid !== binding.pid || info.tcp_port !== endpointPort)
151
154
  fail('binding', 'Context endpoint belongs to a different native process.');
155
+ if (context)
156
+ await recordLifecycle(this.workspace, context, { at: new Date().toISOString(), event: 'start', reason: 'native_launch', pid: binding.pid });
152
157
  return record;
153
158
  }
154
159
  catch {
@@ -162,6 +167,11 @@ export class GhidraContexts {
162
167
  child.kill('SIGTERM');
163
168
  for (let i = 0; i < 100 && child.exitCode === null && child.signalCode === null; i++)
164
169
  await delay(50);
170
+ if (context && (child.exitCode !== null || child.signalCode !== null)) {
171
+ const at = new Date().toISOString();
172
+ await durable(join(root, 'stopped.json'), { pid: binding.pid, tokenSecret: binding.tokenSecret, at, reason: 'startup_failed' });
173
+ await recordLifecycle(this.workspace, context, { at, event: 'stop', reason: 'startup_failed', pid: binding.pid });
174
+ }
165
175
  this.running.delete(root);
166
176
  throw error;
167
177
  }
@@ -190,7 +200,7 @@ export class GhidraContexts {
190
200
  fail('binding', 'Opened native program does not match the target input bytes.');
191
201
  return object(await record.http.call('/get_metadata', { program: record.binding.program }));
192
202
  }
193
- async stopRecord(record, ready = true, releaseCapacity = true) {
203
+ async stopRecord(record, ready = true, releaseCapacity = true, reason = 'temporary_complete') {
194
204
  const { binding, child } = record;
195
205
  const childFinished = child && child.pid === binding.pid && (child.exitCode !== null || child.signalCode !== null);
196
206
  if (!childFinished && alive(binding.pid)) {
@@ -213,7 +223,10 @@ export class GhidraContexts {
213
223
  }
214
224
  if (binding.mode === 'gui')
215
225
  await this.debuggerReleased(binding);
216
- await durable(join(binding.root, 'stopped.json'), { pid: binding.pid, tokenSecret: binding.tokenSecret });
226
+ const at = new Date().toISOString();
227
+ await durable(join(binding.root, 'stopped.json'), { pid: binding.pid, tokenSecret: binding.tokenSecret, at, reason });
228
+ if (binding.context)
229
+ await recordLifecycle(this.workspace, binding.context, { at, event: 'stop', reason, pid: binding.pid });
217
230
  if (releaseCapacity && binding.context)
218
231
  this.capacity.slots.delete(binding.context);
219
232
  this.running.delete(binding.root);
@@ -378,11 +391,13 @@ export class GhidraContexts {
378
391
  record.binding.viewEpoch = context.viewEpoch ?? 0;
379
392
  await durable(join(root, 'process.json'), record.binding);
380
393
  await durable(join(this.directory(), 'context', context.id, 'current.json'), record.binding);
394
+ if (context.workingSnapshot)
395
+ await recordLifecycle(this.workspace, context.id, { at: new Date().toISOString(), event: 'restore', reason: 'saved_snapshot', pid: record.binding.pid });
381
396
  return record;
382
397
  }
383
398
  catch (error) {
384
399
  if (record)
385
- await this.stopRecord(record, false);
400
+ await this.stopRecord(record, false, true, 'startup_failed');
386
401
  else
387
402
  this.capacity.slots.delete(context.id);
388
403
  throw error;
@@ -413,7 +428,7 @@ export class GhidraContexts {
413
428
  }
414
429
  if (binding && !stopped && alive(binding.pid) && ((binding.viewEpoch ?? 0) !== (context.viewEpoch ?? 0) ||
415
430
  binding.baseRevision !== context.baseRevision && context.baseRevision !== undefined)) {
416
- await this.stopRecord(await this.verified(binding, target), true, false);
431
+ await this.stopRecord(await this.verified(binding, target), true, false, 'view_replaced');
417
432
  stopped = true;
418
433
  }
419
434
  if (binding && !stopped && alive(binding.pid)) {
@@ -434,6 +449,8 @@ export class GhidraContexts {
434
449
  const snapshot = context.workingSnapshot ?? model.sharedSnapshot;
435
450
  if (!snapshot || !model.sharedRevision)
436
451
  fail('binding', 'Canonical target has no saved base.');
452
+ if (binding && !stopped && !alive(binding.pid))
453
+ await recordLifecycle(this.workspace, context.id, { at: new Date().toISOString(), event: 'stop', reason: 'unknown', pid: binding.pid });
437
454
  await this.privateCopy(context, snapshot);
438
455
  if (context.workingSnapshot)
439
456
  return this.workspace.saveContext(scope, context.generation, { state: 'ready' });
@@ -447,7 +464,19 @@ export class GhidraContexts {
447
464
  fail('binding', 'Context process binding changed.');
448
465
  return this.verified(binding, this.workspace.target(scope.target));
449
466
  }
450
- async observation(scope) { return this.call(scope, '/research_view'); }
467
+ async observation(scope) {
468
+ const record = await this.selected(scope);
469
+ return { ...object(await this.call(scope, '/research_view')), incarnation: digest({ pid: record.binding.pid, root: record.binding.root }) };
470
+ }
471
+ async inspectObservation(scope) {
472
+ const record = await this.selected(scope);
473
+ const view = researchView(await record.http.call('/insight/research-view/v1', { program: record.binding.program }));
474
+ return { ...view, incarnation: digest({ pid: record.binding.pid, root: record.binding.root }) };
475
+ }
476
+ async inlineScript(scope, parameters, requestId) {
477
+ const record = await this.selected(scope);
478
+ return runInline(record.http, record.binding.program, record.binding.root, parameters, requestId);
479
+ }
451
480
  async call(scope, operation, parameters = {}) {
452
481
  if (Object.hasOwn(parameters, 'program'))
453
482
  fail('invalid', 'Program routing is owned by the session and target selectors.');
@@ -652,7 +681,7 @@ public class ${name} extends GhidraScript {
652
681
  const debuggerState = record.binding.mode === 'gui' && context.debugger?.traces.length ? await this.debuggerSaved(scope) : context.debugger;
653
682
  const complexChanges = await this.debuggerMetadataChanges(context, snapshot);
654
683
  this.workspace.saveContext(scope, context.generation, { workingSnapshot: snapshot, complexChanges, ...(debuggerState ? { debugger: { ...debuggerState, connected: false, traces: debuggerState.traces.map(trace => ({ ...trace, connected: false })) } } : {}) });
655
- await this.stopRecord(record, true, false);
684
+ await this.stopRecord(record, true, false, 'mode_changed');
656
685
  if (mode === 'headless') {
657
686
  await this.privateCopy(this.workspace.context(scope), snapshot);
658
687
  return;
@@ -868,7 +897,7 @@ public class ${name} extends GhidraScript {
868
897
  const debuggerState = record.binding.mode === 'gui' && context.debugger?.traces.length ? await this.debuggerSaved(scope) : context.debugger;
869
898
  const complexChanges = await this.debuggerMetadataChanges(context, snapshot);
870
899
  this.workspace.saveContext(scope, context.generation, { state: 'stopped', workingSnapshot: snapshot, complexChanges, ...(debuggerState ? { debugger: { ...debuggerState, connected: false, traces: debuggerState.traces.map(trace => ({ ...trace, connected: false })) } } : {}) });
871
- await this.stopRecord(record);
900
+ await this.stopRecord(record, true, true, 'explicit_close');
872
901
  }
873
902
  async closed(scope) {
874
903
  const context = this.workspace.context(scope), path = join(this.directory(), 'context', context.id, 'current.json');
@@ -1050,7 +1079,7 @@ public class ${name} extends GhidraScript {
1050
1079
  for (const record of [...this.running.values()]) {
1051
1080
  try {
1052
1081
  // Original child handles also permit cleanup of a failed, unready owned stage.
1053
- await cleanup.stopRecord(record, !record.child);
1082
+ await cleanup.stopRecord(record, !record.child, true, 'owner_dispose');
1054
1083
  }
1055
1084
  catch {
1056
1085
  failures++;
@@ -0,0 +1,9 @@
1
+ import type { LocatedEntry } from './function-entries.js';
2
+ /** Validate evidence and selection policy independently, before any function inspection/save. */
3
+ export declare function locatedBoundaries(raw: unknown, request: {
4
+ addresses: string[];
5
+ unwindMaximum: number;
6
+ }, modification: string): {
7
+ entries: Map<string, LocatedEntry>;
8
+ boundaryLookup: Record<string, unknown>;
9
+ };