@eyeauras/insight-cli 0.3.2 → 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 (140) hide show
  1. package/.cli-package-build.json +1 -1
  2. package/SKILL.md +4 -1
  3. package/dist/src/arguments.js +9 -4
  4. package/dist/src/cli.js +45 -37
  5. package/dist/src/code-references.d.ts +48 -0
  6. package/dist/src/code-references.js +154 -0
  7. package/dist/src/context-diagnostics.d.ts +25 -0
  8. package/dist/src/context-diagnostics.js +82 -0
  9. package/dist/src/contexts.d.ts +13 -1
  10. package/dist/src/contexts.js +38 -9
  11. package/dist/src/function-boundaries.d.ts +9 -0
  12. package/dist/src/function-boundaries.js +68 -0
  13. package/dist/src/function-entries.d.ts +33 -0
  14. package/dist/src/function-entries.js +84 -0
  15. package/dist/src/function-inspection.d.ts +4 -2
  16. package/dist/src/function-inspection.js +94 -59
  17. package/dist/src/function-results.d.ts +3 -1
  18. package/dist/src/function-results.js +9 -3
  19. package/dist/src/inline-script.d.ts +15 -0
  20. package/dist/src/inline-script.js +62 -0
  21. package/dist/src/internal-timing.d.ts +3 -0
  22. package/dist/src/internal-timing.js +52 -0
  23. package/dist/src/lifecycle.js +14 -4
  24. package/dist/src/native-inputs.d.ts +1 -1
  25. package/dist/src/native-inputs.js +1 -1
  26. package/dist/src/native-queries.js +1 -1
  27. package/dist/src/persistent-client.d.ts +30 -0
  28. package/dist/src/persistent-client.js +125 -0
  29. package/dist/src/queries.js +4 -1
  30. package/dist/src/query-policy.js +9 -1
  31. package/dist/src/research-view.d.ts +6 -0
  32. package/dist/src/research-view.js +9 -0
  33. package/dist/src/research.d.ts +38 -0
  34. package/dist/src/research.js +78 -8
  35. package/dist/src/string-anchors.d.ts +43 -0
  36. package/dist/src/string-anchors.js +176 -0
  37. package/dist/src/sync-policy.js +3 -0
  38. package/dist/src/timed-backend.d.ts +3 -0
  39. package/dist/src/timed-backend.js +16 -0
  40. package/dist/src/transport.d.ts +1 -0
  41. package/dist/src/transport.js +4 -0
  42. package/dist/src/workspace.js +5 -2
  43. package/package.json +12 -1
  44. package/references/native/InsightAnchorScan.java +135 -0
  45. package/references/native/InsightCodeReferences.java +129 -0
  46. package/references/native/InsightFunctionBoundaries.java +56 -0
  47. package/references/native/InsightInlineExecutor.java +143 -0
  48. package/references/native/InsightInlineScript.java +14 -0
  49. package/references/native/InsightPeRanges.java +92 -0
  50. package/references/native/InsightResearchQueries.java +22 -1
  51. package/references/native/InsightResearchView.java +69 -0
  52. package/references/native/InsightStringAnchors.java +49 -0
  53. package/references/persistent-client.mjs +125 -0
  54. package/references/persistent.md +50 -0
  55. package/references/query-semantics.md +12 -0
  56. package/references/recovery.md +36 -0
  57. package/references/research.md +87 -0
  58. package/references/setup.md +1 -1
  59. package/references/sparse-navigation.md +173 -0
  60. package/runtime/GhidraMCP-7.0.0.jar +0 -0
  61. package/runtime/GhidraMCP-NOTICE +5 -0
  62. package/runtime/provenance.json +17 -3
  63. package/runtime/research-view.provenance.json +28 -0
  64. package/runtime/toolbox.json +10 -3
  65. package/skill-bundle/insight-artifacts/SKILL.md +1 -1
  66. package/skill-bundle/insight-artifacts/references/recovery.md +36 -0
  67. package/skill-bundle/insight-binary/SKILL.md +1 -1
  68. package/skill-bundle/insight-capture/SKILL.md +1 -1
  69. package/skill-bundle/insight-cli/SKILL.md +4 -1
  70. package/skill-bundle/insight-cli/references/persistent-client.mjs +125 -0
  71. package/skill-bundle/insight-cli/references/persistent.md +50 -0
  72. package/skill-bundle/insight-cli/references/recovery.md +36 -0
  73. package/skill-bundle/insight-cli/references/setup.md +1 -1
  74. package/skill-bundle/insight-die/SKILL.md +1 -1
  75. package/skill-bundle/insight-dnspy/SKILL.md +1 -1
  76. package/skill-bundle/insight-dotnet-dump/SKILL.md +1 -1
  77. package/skill-bundle/insight-dotnet-stack/SKILL.md +1 -1
  78. package/skill-bundle/insight-dotnet-trace/SKILL.md +1 -1
  79. package/skill-bundle/insight-frida/SKILL.md +1 -1
  80. package/skill-bundle/insight-ghidra/SKILL.md +14 -1
  81. package/skill-bundle/insight-ghidra/references/persistent-client.mjs +125 -0
  82. package/skill-bundle/insight-ghidra/references/persistent.md +50 -0
  83. package/skill-bundle/insight-ghidra/references/query-semantics.md +12 -0
  84. package/skill-bundle/insight-ghidra/references/recovery.md +36 -0
  85. package/skill-bundle/insight-ghidra/references/research.md +87 -0
  86. package/skill-bundle/insight-ghidra/references/setup.md +1 -1
  87. package/skill-bundle/insight-ghidra/references/sparse-navigation.md +173 -0
  88. package/skill-bundle/insight-ilspy/SKILL.md +1 -1
  89. package/skill-bundle/insight-interop/SKILL.md +1 -1
  90. package/skill-bundle/insight-lldb/SKILL.md +1 -1
  91. package/skill-bundle/insight-llvm/SKILL.md +1 -1
  92. package/skill-bundle/insight-managed-heap/SKILL.md +1 -1
  93. package/skill-bundle/insight-managed-metadata/SKILL.md +1 -1
  94. package/skill-bundle/insight-mitmproxy/SKILL.md +1 -1
  95. package/skill-bundle/insight-overview/SKILL.md +1 -1
  96. package/skill-bundle/insight-procmon/SKILL.md +1 -1
  97. package/skill-bundle/insight-re/SKILL.md +4 -1
  98. package/skill-bundle/insight-re/references/persistent-client.mjs +125 -0
  99. package/skill-bundle/insight-re/references/persistent.md +50 -0
  100. package/skill-bundle/insight-re/references/recovery.md +36 -0
  101. package/skill-bundle/insight-sevenzip/SKILL.md +1 -1
  102. package/skill-bundle/insight-strace/SKILL.md +1 -1
  103. package/skill-bundle/insight-tshark/SKILL.md +1 -1
  104. package/skill-bundle/insight-windbg/SKILL.md +1 -1
  105. package/skill-bundle/manifest.json +1 -1
  106. package/skills/insight-artifacts/SKILL.md +1 -1
  107. package/skills/insight-artifacts/references/recovery.md +36 -0
  108. package/skills/insight-binary/SKILL.md +1 -1
  109. package/skills/insight-capture/SKILL.md +1 -1
  110. package/skills/insight-die/SKILL.md +1 -1
  111. package/skills/insight-dnspy/SKILL.md +1 -1
  112. package/skills/insight-dotnet-dump/SKILL.md +1 -1
  113. package/skills/insight-dotnet-stack/SKILL.md +1 -1
  114. package/skills/insight-dotnet-trace/SKILL.md +1 -1
  115. package/skills/insight-frida/SKILL.md +1 -1
  116. package/skills/insight-ghidra/SKILL.md +14 -1
  117. package/skills/insight-ghidra/references/persistent-client.mjs +125 -0
  118. package/skills/insight-ghidra/references/persistent.md +50 -0
  119. package/skills/insight-ghidra/references/query-semantics.md +12 -0
  120. package/skills/insight-ghidra/references/recovery.md +36 -0
  121. package/skills/insight-ghidra/references/research.md +87 -0
  122. package/skills/insight-ghidra/references/setup.md +1 -1
  123. package/skills/insight-ghidra/references/sparse-navigation.md +173 -0
  124. package/skills/insight-ilspy/SKILL.md +1 -1
  125. package/skills/insight-interop/SKILL.md +1 -1
  126. package/skills/insight-lldb/SKILL.md +1 -1
  127. package/skills/insight-llvm/SKILL.md +1 -1
  128. package/skills/insight-managed-heap/SKILL.md +1 -1
  129. package/skills/insight-managed-metadata/SKILL.md +1 -1
  130. package/skills/insight-mitmproxy/SKILL.md +1 -1
  131. package/skills/insight-overview/SKILL.md +1 -1
  132. package/skills/insight-procmon/SKILL.md +1 -1
  133. package/skills/insight-re/SKILL.md +4 -1
  134. package/skills/insight-re/references/persistent-client.mjs +125 -0
  135. package/skills/insight-re/references/persistent.md +50 -0
  136. package/skills/insight-re/references/recovery.md +36 -0
  137. package/skills/insight-sevenzip/SKILL.md +1 -1
  138. package/skills/insight-strace/SKILL.md +1 -1
  139. package/skills/insight-tshark/SKILL.md +1 -1
  140. package/skills/insight-windbg/SKILL.md +1 -1
@@ -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
+ };
@@ -0,0 +1,68 @@
1
+ import { fail, object } from './model.js';
2
+ const hex = (v) => { if (typeof v !== 'string' || !/^(?:0|[1-9a-f][a-f0-9]{0,15})$/.test(v))
3
+ fail('partial', 'Malformed runtime boundary address.'); return v; };
4
+ const big = (v) => BigInt('0x' + v);
5
+ const unavailable = new Set(['headers_not_mapped', 'unsupported_machine', 'unsupported_optional_header', 'unsupported_language', 'table_not_mapped', 'runtime_range_not_initialized_executable', 'unwind_not_mapped', 'memory_read_failed', 'short_read']);
6
+ const malformed = new Set(['invalid_dos_signature', 'invalid_pe_offset', 'invalid_pe_signature', 'short_optional_header', 'invalid_image_headers', 'address_overflow', 'invalid_rva', 'invalid_exception_directory', 'invalid_runtime_range', 'unordered_or_overlapping_ranges', 'invalid_unwind_rva']);
7
+ /** Validate evidence and selection policy independently, before any function inspection/save. */
8
+ export function locatedBoundaries(raw, request, modification) {
9
+ const v = object(raw), s = object(v.boundaryLookup), items = object(v.items);
10
+ if (v.protocol !== 'insight-function-boundaries-v1' || typeof v.modification !== 'string')
11
+ fail('partial', 'Malformed function-boundary protocol.');
12
+ if (v.modification !== modification)
13
+ fail('binding', 'Native view changed during boundary lookup.', { reason: 'changed_view' });
14
+ const state = String(s.state), complete = state === 'validated' || state === 'absent';
15
+ const reason = String(s.reason), reasonValid = state === 'validated' ? reason === 'validated' : state === 'absent' ? reason === 'no_exception_directory' : state === 'incomplete' ? ['byte_limit', 'timeout'].includes(reason) : state === 'unavailable' ? unavailable.has(reason) : state === 'malformed' && malformed.has(reason);
16
+ if (!reasonValid || s.complete !== complete || s.headersSource !== 'current-mapped-memory' || s.maximumBytes !== request.unwindMaximum || s.deadlineMilliseconds !== 5000)
17
+ fail('partial', 'Malformed function-boundary coverage.');
18
+ for (const key of ['recordCount', 'recordsValidated', 'bytesRead'])
19
+ if (!Number.isSafeInteger(s[key]) || Number(s[key]) < 0)
20
+ fail('partial', 'Malformed boundary counters.');
21
+ if (Number(s.bytesRead) > request.unwindMaximum || Number(s.recordCount) > Math.floor(0xffffffff / 12) || Number(s.recordsValidated) > Number(s.recordCount) || Number(s.recordsValidated) * 12 > Number(s.bytesRead) || state === 'validated' && (s.recordCount !== s.recordsValidated || Number(s.recordCount) < 1) || state === 'absent' && Number(s.recordCount) !== 0)
22
+ fail('partial', 'Inconsistent function-boundary counters.');
23
+ if (complete && Number(s.bytesRead) < (state === 'absent' ? 200 : 208 + 12 * Number(s.recordCount)))
24
+ fail('partial', 'Impossible complete boundary read accounting.');
25
+ if (Object.keys(items).length !== request.addresses.length || request.addresses.some(at => !Object.hasOwn(items, at)))
26
+ fail('binding', 'Boundary selector set changed.', { reason: 'selector_mismatch' });
27
+ const entries = new Map();
28
+ for (const at of request.addresses) {
29
+ const row = object(items[at]);
30
+ let database = null, boundary = null;
31
+ if (row.database !== null) {
32
+ database = object(row.database);
33
+ const entry = hex(database.entryPoint);
34
+ if (!['exact-entry', 'recorded-containing'].includes(String(database.kind)) || database.recordedBodyContainsAddress !== true || (database.kind === 'exact-entry') !== (entry === at))
35
+ fail('partial', 'Malformed recorded boundary facts.');
36
+ }
37
+ if (row.boundary !== null) {
38
+ boundary = object(row.boundary);
39
+ const lo = hex(boundary.begin), hi = boundary.endExclusive === '10000000000000000' ? boundary.endExclusive : hex(boundary.endExclusive);
40
+ hex(boundary.tableRecordAddress);
41
+ if (!complete || state !== 'validated' || Number(s.recordCount) < 1 || big(lo) > big(at) || big(at) >= big(hi) || boundary.source !== 'pe-exception-directory' || boundary.semanticMembership !== 'not-established' || boundary.instructionBoundary !== 'not-checked' || boundary.unwindInfo !== 'not-parsed')
42
+ fail('partial', 'Malformed or unvalidated runtime region.');
43
+ }
44
+ const conflict = boundary && database && boundary.begin !== database.entryPoint;
45
+ const resolution = { requestedAddress: at, kind: 'unresolved', recordedBodyContainsAddress: database !== null, database, boundary };
46
+ if (row.status === 'resolved') {
47
+ const entry = hex(row.entryPoint), kind = boundary ? 'pe-runtime-range' : database?.kind;
48
+ if (!complete || conflict || !kind || row.kind !== kind || entry !== (boundary?.begin ?? database?.entryPoint) || row.recordedBodyContainsAddress !== (database !== null))
49
+ fail('partial', 'Inconsistent runtime entry selection.');
50
+ entries.set(at, { entry, resolution: { ...resolution, kind: String(kind) } });
51
+ }
52
+ else {
53
+ let expected;
54
+ if (!complete)
55
+ expected = 'boundary_lookup_' + state;
56
+ else if (conflict)
57
+ expected = 'boundary_conflict';
58
+ else if (boundary)
59
+ expected = 'runtime_entry_not_defined';
60
+ else
61
+ expected = 'no_runtime_range_or_recorded_function';
62
+ if (row.status !== 'unresolved' || row.reason !== expected || complete && !boundary && database || expected === 'runtime_entry_not_defined' && database || row.entryPoint !== undefined || row.kind !== undefined)
63
+ fail('partial', 'Inconsistent unresolved runtime boundary.');
64
+ entries.set(at, { status: 'partial', reason: expected, resolution });
65
+ }
66
+ }
67
+ return { entries, boundaryLookup: s };
68
+ }
@@ -0,0 +1,33 @@
1
+ export interface EntryOptions {
2
+ resolveAddress?: string;
3
+ maxEntryDistance?: number;
4
+ maxUnwindBytes?: number;
5
+ }
6
+ export interface LegacyEntryRequest {
7
+ addresses: string[];
8
+ maximum: number;
9
+ }
10
+ export type EntryRequest = LegacyEntryRequest | {
11
+ addresses: string[];
12
+ unwindMaximum: number;
13
+ };
14
+ export interface EntryResolution {
15
+ requestedAddress: string;
16
+ kind: string;
17
+ recordedBodyContainsAddress: boolean;
18
+ distanceBytes?: number;
19
+ database?: unknown;
20
+ boundary?: unknown;
21
+ }
22
+ export interface LocatedEntry {
23
+ entry?: string;
24
+ resolution?: EntryResolution;
25
+ reason?: string;
26
+ status?: 'partial';
27
+ }
28
+ export declare function functionSelectors(parameters: Record<string, unknown>): string[];
29
+ export declare function entryAddress(value: unknown): string;
30
+ /** Validate before opening/probing native state; ordinary inspection keeps its original contract. */
31
+ export declare function entryRequest(parameters: Record<string, unknown>, options: EntryOptions): EntryRequest | undefined;
32
+ /** The authenticated native lookup supplies DB facts, never inferred semantic membership. */
33
+ export declare function locatedEntries(raw: unknown, request: LegacyEntryRequest, modification: string): Map<string, LocatedEntry>;
@@ -0,0 +1,84 @@
1
+ import { fail, object } from './model.js';
2
+ export function functionSelectors(parameters) {
3
+ const selectors = String(parameters.functions || parameters.function || '').split(',').map(s => s.trim());
4
+ if (!selectors.length || selectors.length > 20 || selectors.some(s => !s || s.length > 128) || new Set(selectors).size !== selectors.length)
5
+ fail('invalid', 'Select 1–20 distinct function names or addresses.');
6
+ return selectors;
7
+ }
8
+ export function entryAddress(value) {
9
+ if (typeof value !== 'string' || !/^(?:0x)?[a-f0-9]{1,16}$/i.test(value))
10
+ fail('invalid', 'Address resolution requires 1–20 default-space hexadecimal addresses, not names.');
11
+ return BigInt('0x' + value.replace(/^0x/i, '')).toString(16);
12
+ }
13
+ /** Validate before opening/probing native state; ordinary inspection keeps its original contract. */
14
+ export function entryRequest(parameters, options) {
15
+ if (options.resolveAddress === undefined) {
16
+ if (options.maxEntryDistance !== undefined || options.maxUnwindBytes !== undefined)
17
+ fail('invalid', 'Resolution budgets require --resolve-address.');
18
+ return;
19
+ }
20
+ if (options.resolveAddress === 'containing-or-pe-runtime') {
21
+ if (options.maxEntryDistance !== undefined)
22
+ fail('invalid', 'PE runtime resolution excludes --max-entry-distance.');
23
+ const unwindMaximum = options.maxUnwindBytes ?? 4194304;
24
+ if (!Number.isSafeInteger(unwindMaximum) || unwindMaximum < 1 || unwindMaximum > 16777216)
25
+ fail('invalid', '--max-unwind-bytes must be between 1 and 16777216.');
26
+ if (parameters.function && parameters.functions)
27
+ fail('invalid', 'Supply --function or --functions, not both, for address resolution.');
28
+ return { addresses: [...new Set(functionSelectors(parameters).map(entryAddress))], unwindMaximum };
29
+ }
30
+ if (options.maxUnwindBytes !== undefined)
31
+ fail('invalid', '--max-unwind-bytes requires containing-or-pe-runtime.');
32
+ if (options.resolveAddress !== 'containing-or-preceding')
33
+ fail('invalid', '--resolve-address must be containing-or-preceding.');
34
+ const maximum = options.maxEntryDistance;
35
+ if (typeof maximum !== 'number' || !Number.isSafeInteger(maximum) || maximum < 1 || maximum > 65536)
36
+ fail('invalid', 'Address resolution requires --max-entry-distance between 1 and 65536 bytes.');
37
+ if (parameters.function && parameters.functions)
38
+ fail('invalid', 'Supply --function or --functions, not both, for address resolution.');
39
+ return { addresses: [...new Set(functionSelectors(parameters).map(entryAddress))], maximum };
40
+ }
41
+ const misses = new Set(['address_out_of_range', 'address_not_in_initialized_executable_block', 'no_preceding_entry', 'entry_outside_block', 'entry_distance_exceeded']);
42
+ /** The authenticated native lookup supplies DB facts, never inferred semantic membership. */
43
+ export function locatedEntries(raw, request, modification) {
44
+ const result = object(raw), items = object(result.items);
45
+ if (result.protocol !== 'insight-function-entries-v1' || typeof result.modification !== 'string')
46
+ fail('partial', 'Function-entry lookup returned malformed protocol facts.');
47
+ if (result.modification !== modification)
48
+ fail('binding', 'Native view changed during function-entry lookup.', { reason: 'changed_view' });
49
+ if (Object.keys(items).length !== request.addresses.length ||
50
+ request.addresses.some(at => !Object.hasOwn(items, at)))
51
+ fail('binding', 'Function-entry lookup returned another view or selector set.', { reason: 'selector_mismatch' });
52
+ const out = new Map();
53
+ for (const at of request.addresses) {
54
+ const row = object(items[at]);
55
+ if (row.status === 'not_found') {
56
+ if (typeof row.reason !== 'string' || !misses.has(row.reason))
57
+ fail('partial', 'Function-entry lookup returned an invalid policy miss.');
58
+ out.set(at, { reason: row.reason });
59
+ continue;
60
+ }
61
+ if (row.status !== 'resolved' || typeof row.entryPoint !== 'string' || !/^[a-f0-9]{1,16}$/.test(row.entryPoint) ||
62
+ entryAddress(row.entryPoint) !== row.entryPoint || typeof row.recordedBodyContainsAddress !== 'boolean')
63
+ fail('partial', 'Function-entry lookup returned malformed entry facts.');
64
+ const entry = row.entryPoint, contains = row.recordedBodyContainsAddress, kind = row.kind;
65
+ // Address strings are hexadecimal even when they contain digits only.
66
+ const actualDistance = BigInt('0x' + at) - BigInt('0x' + entry);
67
+ if (kind === 'exact-entry') {
68
+ if (entry !== at || !contains || row.distanceBytes !== 0)
69
+ fail('partial', 'Function-entry lookup returned inconsistent exact-entry facts.');
70
+ }
71
+ else if (kind === 'recorded-containing') {
72
+ if (entry === at || !contains || row.distanceBytes !== undefined)
73
+ fail('partial', 'Function-entry lookup returned inconsistent recorded containment.');
74
+ }
75
+ else if (kind === 'preceding-entry-candidate') {
76
+ if (contains || actualDistance < 1n || actualDistance > BigInt(request.maximum) || row.distanceBytes !== Number(actualDistance))
77
+ fail('partial', 'Function-entry lookup returned an out-of-policy candidate.');
78
+ }
79
+ else
80
+ fail('partial', 'Function-entry lookup returned an unknown resolution kind.');
81
+ out.set(at, { entry, resolution: { requestedAddress: at, kind, recordedBodyContainsAddress: contains, ...(kind === 'recorded-containing' ? {} : { distanceBytes: row.distanceBytes }) } });
82
+ }
83
+ return out;
84
+ }
@@ -1,6 +1,7 @@
1
1
  import type { ResearchBackend } from './research.js';
2
2
  import type { Scope } from './workspace.js';
3
- export interface FunctionInspectionOptions {
3
+ import { type EntryOptions } from './function-entries.js';
4
+ export interface FunctionInspectionOptions extends EntryOptions {
4
5
  limit: number;
5
6
  offset: number;
6
7
  total: number;
@@ -10,7 +11,7 @@ export interface FunctionInspectionOptions {
10
11
  export declare function inspectFunctions(backend: ResearchBackend, scope: Scope, parameters: Record<string, unknown>, options: FunctionInspectionOptions, view: {
11
12
  revision?: string;
12
13
  identity: string;
13
- }, observation?: unknown): Promise<{
14
+ }, observation?: unknown, observe?: () => Promise<unknown>): Promise<{
14
15
  value: {
15
16
  save?: {
16
17
  host: string;
@@ -25,6 +26,7 @@ export declare function inspectFunctions(backend: ResearchBackend, scope: Scope,
25
26
  maximumCharacters: number;
26
27
  returnedCharacters: number;
27
28
  };
29
+ boundaryLookup?: Record<string, unknown> | undefined;
28
30
  };
29
31
  session: string;
30
32
  target: string;
@@ -2,77 +2,112 @@ import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import { resolve, join, isAbsolute } from 'node:path';
3
3
  import { fail, object, digest } from './model.js';
4
4
  import { functionResult } from './function-results.js';
5
+ import { timed } from './internal-timing.js';
5
6
  import { textWindow } from './text-window.js';
6
7
  import { CliError } from '@eyeauras/cli-factory';
8
+ import { entryAddress, entryRequest, functionSelectors, locatedEntries } from './function-entries.js';
9
+ import { locatedBoundaries } from './function-boundaries.js';
7
10
  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) {
11
+ export async function inspectFunctions(backend, scope, parameters, options, view, observation, observe) {
9
12
  if (options.saveDirectory !== undefined && !isAbsolute(options.saveDirectory))
10
13
  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 selectors = functionSelectors(parameters), lookup = entryRequest(parameters, options);
14
15
  const selectedFields = String(parameters.fields ?? '').split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
15
16
  if (selectedFields.some(s => !fields.has(s)))
16
17
  fail('invalid', 'Unknown function field. Allowed fields: ' + [...fields].join(', '));
17
18
  if (options.expectView !== undefined && options.expectView !== view.identity)
18
19
  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' };
20
+ const observeAfter = observe ?? (backend.observation ? () => backend.observation(scope) : undefined);
21
+ let entries, boundaryLookup, raw, nativeSelectors;
22
+ const guard = async () => { if (observeAfter && digest(observation) !== digest(await observeAfter()))
23
+ fail('binding', 'Native view changed during bulk function inspection.', { reason: 'changed_view', view }); };
24
+ try {
25
+ if (lookup) {
26
+ if (!observeAfter || typeof object(observation ?? {}).modification !== 'string')
27
+ fail('unsupported', 'Address resolution requires guarded native observations.');
28
+ if ('unwindMaximum' in lookup) {
29
+ const result = locatedBoundaries(await backend.call(scope, '/insight/function-boundaries/v1', { addresses: lookup.addresses.join(','), max_unwind_bytes: lookup.unwindMaximum }), lookup, String(object(observation).modification));
30
+ entries = result.entries;
31
+ boundaryLookup = result.boundaryLookup;
53
32
  }
33
+ else
34
+ entries = locatedEntries(await backend.call(scope, '/insight/function-entries/v1', { addresses: lookup.addresses.join(','), max_entry_distance: lookup.maximum }), lookup, String(object(observation).modification));
54
35
  }
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;
36
+ nativeSelectors = entries ? [...new Set([...entries.values()].flatMap(row => row.entry ? [row.entry] : []))] : selectors;
37
+ const nativeParameters = { ...parameters, ...(entries ? { function: '' } : {}), functions: nativeSelectors.join(','), ...(entries && selectedFields.length ? { fields: [...new Set([...selectedFields, 'entry_point'])].join(',') } : {}) };
38
+ raw = nativeSelectors.length ? object(await backend.call(scope, '/get_functions', nativeParameters)) : { functions: {} };
60
39
  }
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);
40
+ catch (error) {
41
+ await guard();
42
+ throw error;
65
43
  }
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 };
44
+ await guard();
45
+ return timed('adapter.normalize', 'functions.inspect', async () => {
46
+ const functions = object(raw.functions);
47
+ if (Object.keys(functions).length !== nativeSelectors.length || nativeSelectors.some(s => !Object.hasOwn(functions, s)))
48
+ fail('partial', 'Bulk function selectors changed.');
49
+ const output = Object.create(null), saved = [];
50
+ let remaining = options.total;
51
+ for (const [index, selector] of selectors.entries()) {
52
+ const located = entries?.get(entryAddress(selector)), selected = located?.entry ?? selector;
53
+ const provenance = located?.resolution ? { ...(located.entry ? { entry_point: located.entry } : {}), resolution: { ...located.resolution, requestedAddress: selector } } : {};
54
+ if (located?.reason) {
55
+ output[selector] = { status: located.status ?? 'not_found', selector, reason: located.reason, error: 'No function was selected by the explicit address-resolution policy.', ...provenance };
56
+ continue;
57
+ }
58
+ let found;
59
+ try {
60
+ found = functionResult({ functions: { [selected]: functions[selected] } }, selected, { requireEntryPoint: Boolean(entries) || !selectedFields.length || selectedFields.includes('entry_point') });
61
+ }
62
+ catch (error) {
63
+ if (!(error instanceof CliError))
64
+ throw error;
65
+ const detail = object(error.result);
66
+ output[selector] = { error: error.message, ...detail, ...provenance, ...(entries ? { selector } : {}), status: detail.reason === 'function_not_found' ? 'not_found' : 'failed' };
67
+ continue;
68
+ }
69
+ if (located?.entry && entryAddress(found.entry_point) !== located.entry)
70
+ fail('binding', 'Inspected function differs from the resolved entry.', { reason: 'selector_mismatch' });
71
+ const row = { ...found, ...provenance, status: 'success' };
72
+ if (typeof row.decompiled_code === 'string') {
73
+ 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));
74
+ const { content, ...metadata } = window;
75
+ remaining -= Array.from(content).length;
76
+ row.decompiled_code = content;
77
+ row.text = { ...metadata, returnedCharacters: Array.from(content).length, nextOffset: Math.min(window.totalCharacters, window.offset + Array.from(content).length) };
78
+ if (options.saveDirectory) {
79
+ if (Buffer.byteLength(source) > 4 * 1024 ** 2)
80
+ fail('limit', 'Native function text exceeds the four MiB saved-file bound.');
81
+ const name = String(index).padStart(2, '0') + '-' + window.textSha256 + '.c';
82
+ saved.push({ name, content: source });
83
+ delete row.decompiled_code;
84
+ row.savedText = { file: join(resolve(options.saveDirectory), name), textSha256: window.textSha256, totalCharacters: window.totalCharacters, sourceTruncated: window.sourceTruncated, coverage: 'entire returned native text' };
85
+ }
86
+ }
87
+ else if (!selectedFields.length || selectedFields.includes('decompiled_code')) {
88
+ row.status = 'partial';
89
+ row.text = { reason: 'missing_decompiler_text' };
90
+ }
91
+ output[selector] = row;
92
+ }
93
+ if (!parameters.functions) {
94
+ const single = object(output[selectors[0]]);
95
+ if (single.status === 'failed' || single.status === 'not_found' || boundaryLookup && single.reason)
96
+ fail('partial', String(single.error), { ...single, ...(boundaryLookup ? { boundaryLookup } : {}), view });
97
+ }
98
+ const result = { ...(parameters.functions ? { ...(entries ? (typeof raw.program === 'string' ? { program: raw.program } : {}) : raw), functions: output } : object(output[selectors[0]])), ...(boundaryLookup ? { boundaryLookup } : {}), 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') } } : {}) };
99
+ if (Buffer.byteLength(JSON.stringify(result)) > 1048576)
100
+ fail('limit', 'Function response exceeds one MiB; narrow fields or selectors.');
101
+ if (options.saveDirectory) {
102
+ // A fresh directory prevents overwrite and makes a partial filesystem failure observable.
103
+ await timed('adapter.save-text', 'functions.inspect', async () => {
104
+ const directory = resolve(options.saveDirectory);
105
+ await mkdir(directory, { recursive: false });
106
+ for (const file of saved)
107
+ await writeFile(join(directory, file.name), file.content, { flag: 'wx', mode: 0o600 });
108
+ await writeFile(join(directory, 'manifest.json'), JSON.stringify({ ...scope, value: result }, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
109
+ });
110
+ }
111
+ return { ...scope, value: result };
112
+ });
78
113
  }
@@ -1,2 +1,4 @@
1
1
  export declare function functionFailure(selector: string, reason: string, diagnostic?: unknown): never;
2
- export declare function functionResult(value: unknown, selector: string): Record<string, unknown>;
2
+ export declare function functionResult(value: unknown, selector: string, options?: {
3
+ requireEntryPoint?: boolean;
4
+ }): Record<string, unknown>;
@@ -2,9 +2,11 @@ import { fail } from './model.js';
2
2
  const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
3
3
  export function functionFailure(selector, reason, diagnostic) {
4
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 } : {}) });
5
+ const recovery = reason === 'function_not_found' ? ' For a sparse interior address, use ghidra functions inspect --functions <address> --resolve-address containing-or-preceding --max-entry-distance <bytes> with the same session/target; a preceding entry is only a candidate. For known executable bytes without a function definition, use ghidra functions decode --address <hex> --max-bytes 64 --max-instructions 16 with the same session/target. Decode is read-only and does not create functions or xrefs.' :
6
+ reason === 'native_function_failure' || reason === 'missing_decompiler_text' ? ' For an existing function entry, try ghidra functions force-decompile --function <entry> with the same session/target. This refreshes decompiler output, not Listing instructions or stored xrefs; use functions decode for bounded local bytes.' : '';
7
+ fail('partial', 'Function inspection failed: ' + reason + '.' + recovery, { selector, reason, ...(nativeDiagnostic ? { nativeDiagnostic } : {}) });
6
8
  }
7
- export function functionResult(value, selector) {
9
+ export function functionResult(value, selector, options = {}) {
8
10
  if (!record(value) || !record(value.functions))
9
11
  functionFailure(selector, 'invalid_native_shape');
10
12
  const functions = value.functions;
@@ -15,7 +17,11 @@ export function functionResult(value, selector) {
15
17
  functionFailure(selector, 'invalid_native_shape');
16
18
  if (found.error !== undefined)
17
19
  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))
20
+ if (typeof found.name !== 'string' || !found.name)
19
21
  functionFailure(selector, 'invalid_native_shape');
22
+ if (options.requireEntryPoint !== false || found.entry_point !== undefined) {
23
+ if (typeof found.entry_point !== 'string' || found.entry_point.length > 128 || !/^(?:[A-Za-z0-9_]+:)?(?:0x)?[a-fA-F0-9]+$/.test(found.entry_point))
24
+ functionFailure(selector, 'invalid_native_shape');
25
+ }
20
26
  return found;
21
27
  }
@@ -0,0 +1,15 @@
1
+ import type { HttpBridge } from './transport.js';
2
+ export declare const inlineSourceHash: (code: string) => string;
3
+ /** Native facts come from the fixed helper's receipt, never from caller console output. */
4
+ export declare function inlineReceipt(raw: unknown, expected: {
5
+ attemptId: string;
6
+ requestId: string;
7
+ sourceSha256: string;
8
+ }): {
9
+ response: Record<string, unknown>;
10
+ safe: boolean;
11
+ };
12
+ export declare function runInline(http: HttpBridge, program: string, root: string, parameters: Record<string, unknown>, requestId: string): Promise<{
13
+ response: Record<string, unknown>;
14
+ safe: boolean;
15
+ }>;