@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
@@ -0,0 +1,176 @@
1
+ import { command, Permission } from '@eyeauras/cli-factory';
2
+ import { scope, number, address } from './research.js';
3
+ import { selectedOptions } from './arguments.js';
4
+ import { fail, object, digest, stable } from './model.js';
5
+ import { parseDomainJson } from './json.js';
6
+ export const anchorAlgorithm = 'raw-string-anchors-v1';
7
+ export const anchorEncodings = ['ascii', 'utf16le'];
8
+ const defaults = { max_bytes: 4194304, max_candidates: 10000, timeout_seconds: 5, limit: 100 };
9
+ const big = (s) => BigInt('0x' + s);
10
+ const hex = (v) => { const s = address(v); if (!/^[a-f0-9]{1,16}$/.test(s))
11
+ fail('invalid', 'String anchors require default-space unsigned addresses.'); return s; };
12
+ const bytes = (s, e) => Buffer.from(s, e === 'ascii' ? 'ascii' : 'utf16le');
13
+ function wellFormed(s) { for (let i = 0; i < s.length; i++) {
14
+ const c = s.charCodeAt(i);
15
+ if (c >= 0xd800 && c <= 0xdbff) {
16
+ const next = s.charCodeAt(++i);
17
+ if (!(next >= 0xdc00 && next <= 0xdfff))
18
+ return false;
19
+ }
20
+ else if (c >= 0xdc00 && c <= 0xdfff)
21
+ return false;
22
+ } return true; }
23
+ function query(raw) {
24
+ const q = object(raw), text = q.text;
25
+ if (typeof text !== 'string' || !text.length || text.length > 256 || !wellFormed(text) || text.includes('\0'))
26
+ fail('invalid', 'Supply a literal of 1–256 UTF-16 units, without NUL or unpaired surrogates.');
27
+ if (!Array.isArray(q.encodings) || q.encodings.length < 1 || q.encodings.length > 2)
28
+ fail('invalid', 'Select ascii, utf16le, or both.');
29
+ const encodings = anchorEncodings.filter(e => q.encodings.includes(e));
30
+ if (encodings.length !== q.encodings.length)
31
+ fail('invalid', 'Encodings must be distinct supported values.');
32
+ if (encodings.includes('ascii') && /[^\x00-\x7f]/.test(text))
33
+ fail('invalid', 'Text is not representable in ASCII; select utf16le only.');
34
+ const cap = number(q.cap, 8192, 16);
35
+ if (cap % 2 || encodings.some(e => bytes(text, e).length > cap))
36
+ fail('invalid', '--max-string-bytes must be even and fit every encoded needle.');
37
+ const start = hex(q.start), end = hex(q.end);
38
+ if (big(start) > big(end))
39
+ fail('invalid', 'scan-through must not precede scan-from.');
40
+ return { text, encodings, start, end, cap };
41
+ }
42
+ function budgets(o, base = defaults) { return { max_bytes: number(o.maxBytes ?? base.max_bytes, 16777216, 32768), max_candidates: number(o.maxCandidates ?? base.max_candidates, 100000, 1), timeout_seconds: number(o.timeoutSeconds ?? base.timeout_seconds, 30, 1), limit: number(o.limit ?? base.limit, 1000, 1) }; }
43
+ function frontier(raw, q) {
44
+ const f = object(raw), at = hex(f.address);
45
+ if (big(at) < big(q.start) || big(at) > big(q.end) || !q.encodings.includes(f.encoding))
46
+ fail('invalid', 'String-anchor frontier is outside the query.');
47
+ return { address: at, encoding: f.encoding };
48
+ }
49
+ function ordinal(f, q) { return big(f.address) * BigInt(q.encodings.length) + BigInt(q.encodings.indexOf(f.encoding)); }
50
+ export function anchorRequest(o, who) {
51
+ if (o.cursor !== undefined) {
52
+ if (['text', 'encodings', 'scanFrom', 'scanThrough', 'maxStringBytes'].some(k => o[k] !== undefined))
53
+ fail('invalid', 'Cursor excludes text, encodings, range and context-cap selectors.');
54
+ if (typeof o.cursor !== 'string' || o.cursor.length > 16384 || !/^sa1\.[A-Za-z0-9_-]+$/.test(o.cursor))
55
+ fail('invalid', 'Malformed string-anchor cursor.');
56
+ let c;
57
+ try {
58
+ const encoded = o.cursor.slice(4), b = Buffer.from(encoded, 'base64url');
59
+ if (b.toString('base64url') !== encoded)
60
+ throw Error();
61
+ c = object(parseDomainJson(b.toString('utf8')));
62
+ }
63
+ catch {
64
+ fail('invalid', 'Malformed string-anchor cursor.');
65
+ }
66
+ if (c.version !== 1 || c.algorithm !== anchorAlgorithm || stable(c.scope) !== stable(who) || typeof c.view !== 'string' || !/^[a-f0-9]{64}$/.test(c.view))
67
+ fail('invalid', 'Cursor scope, algorithm or view is incompatible.');
68
+ const q = query(c.query), b = object(c.budgets);
69
+ if (['max_bytes', 'max_candidates', 'timeout_seconds', 'limit'].some(k => !Number.isSafeInteger(b[k])))
70
+ fail('invalid', 'Malformed cursor budgets.');
71
+ return { query: q, from: frontier(c.next, q), expectedView: c.view, budgets: budgets(o, budgets({ maxBytes: b.max_bytes, maxCandidates: b.max_candidates, timeoutSeconds: b.timeout_seconds, limit: b.limit })) };
72
+ }
73
+ if (typeof o.text !== 'string' || typeof o.encodings !== 'string' || typeof o.scanFrom !== 'string' || typeof o.scanThrough !== 'string')
74
+ fail('invalid', 'Supply --text, --encodings, --scan-from and --scan-through, or --cursor.');
75
+ const q = query({ text: o.text, encodings: o.encodings.split(',').map(x => x.trim()), start: o.scanFrom, end: o.scanThrough, cap: o.maxStringBytes ?? 1024 });
76
+ return { query: q, from: { address: q.start, encoding: q.encodings[0] }, budgets: budgets(o) };
77
+ }
78
+ function decoded(raw, e) {
79
+ if (e === 'ascii')
80
+ return raw.some(x => x > 127) ? null : raw.toString('ascii');
81
+ try {
82
+ return new TextDecoder('utf-16le', { fatal: true, ignoreBOM: true }).decode(raw);
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ }
88
+ export function anchorResult(raw, r, modification) {
89
+ const v = object(raw), s = object(v.scan), q = r.query, b = r.budgets;
90
+ if (v.algorithm !== anchorAlgorithm || stable(v.encodings) !== stable(q.encodings) || v.modification !== modification) {
91
+ if (typeof v.modification === 'string' && v.modification !== modification)
92
+ fail('binding', 'Native view changed during string-anchor scan.', { reason: 'changed_view' });
93
+ fail('partial', 'Malformed string-anchor protocol facts.');
94
+ }
95
+ if (!Array.isArray(v.matches) || v.matches.length > b.limit || stable(s.from) !== stable(r.from) || s.through !== q.end || typeof s.complete !== 'boolean' || s.complete !== (s.reason === 'exhausted') || !['exhausted', 'byte_limit', 'candidate_limit', 'timeout', 'result_limit'].includes(String(s.reason)))
96
+ fail('partial', 'Malformed string-anchor coverage.');
97
+ for (const k of ['bytesRead', 'testedPairs', 'candidates'])
98
+ if (!Number.isSafeInteger(s[k]) || Number(s[k]) < 0)
99
+ fail('partial', 'Malformed string-anchor counters.');
100
+ if (Number(s.bytesRead) > b.max_bytes || Number(s.testedPairs) > Number(s.bytesRead) * q.encodings.length || Number(s.candidates) > b.max_candidates || Number(s.candidates) < v.matches.length || Number(s.candidates) > Number(s.testedPairs) + 1 || !Number.isFinite(s.elapsedMs) || Number(s.elapsedMs) < 0)
101
+ fail('partial', 'Inconsistent string-anchor budgets.');
102
+ const next = s.complete ? null : frontier(s.next, q);
103
+ if (s.complete && s.next !== null || next && ordinal(next, q) < ordinal(r.from, q) || next && BigInt(Number(s.testedPairs)) > ordinal(next, q) - ordinal(r.from, q))
104
+ fail('partial', 'String-anchor frontier changed.');
105
+ if (next && ordinal(next, q) === ordinal(r.from, q) && s.reason !== 'timeout')
106
+ fail('partial', 'String-anchor scan made no progress despite its minimum work budget.');
107
+ let previous = ordinal(r.from, q) - 1n;
108
+ for (const item of v.matches) {
109
+ const row = object(item), at = hex(row.matchAddress), encoding = row.encoding;
110
+ if (!q.encodings.includes(encoding))
111
+ fail('partial', 'Unknown string-anchor encoding.');
112
+ const pair = { address: at, encoding }, order = ordinal(pair, q), needle = bytes(q.text, encoding), unit = encoding === 'ascii' ? 1 : 2, l = object(row.literalCandidate);
113
+ if (order <= previous || order < ordinal(r.from, q) || next && order >= ordinal(next, q) || big(at) + BigInt(needle.length - 1) > big(q.end) || row.matchByteLength !== needle.length)
114
+ fail('partial', 'String-anchor pair/order changed.');
115
+ previous = order;
116
+ if (typeof l.textBytes !== 'string' || !/^(?:[a-f0-9]{2})+$/.test(l.textBytes) || l.textBytes.length > q.cap * 2)
117
+ fail('partial', 'String-anchor context bytes are missing or unbounded.');
118
+ const data = Buffer.from(l.textBytes, 'hex'), from = hex(l.textFrom), offset = big(at) - big(from), left = l.start === null ? null : hex(l.start), right = l.terminatorAddress === null ? null : hex(l.terminatorAddress);
119
+ if (offset < 0n || offset > BigInt(q.cap) || Number(offset) % unit || data.length % unit || l.textByteLength !== data.length || big(from) < big(q.start) || big(from) + BigInt(data.length - 1) > big(q.end) || !data.subarray(Number(offset), Number(offset) + needle.length).equals(needle))
120
+ fail('partial', 'String-anchor preview does not contain its byte match.');
121
+ for (let i = 0; i < data.length; i += unit)
122
+ if (data[i] === 0 && (unit === 1 || data[i + 1] === 0))
123
+ fail('partial', 'String-anchor preview crossed a NUL boundary.');
124
+ const boundaries = ['nul', 'range_edge', 'memory_edge', 'string_limit'];
125
+ if (!boundaries.includes(String(l.leftBoundary)) || !boundaries.includes(String(l.rightBoundary)) || (l.leftBoundary === 'nul') !== (left !== null) || (l.rightBoundary === 'nul') !== (right !== null))
126
+ fail('partial', 'String-anchor boundary facts changed.');
127
+ if (left !== null && (big(left) > big(at) || big(left) < big(q.start) + BigInt(unit) || big(left) > big(from) || row.matchOffsetBytes !== Number(big(at) - big(left)) || Number(big(at) - big(left)) % unit || Number(big(at) - big(left)) > q.cap - needle.length) || left === null && row.matchOffsetBytes !== null)
128
+ fail('partial', 'Invalid candidate start/offset.');
129
+ if (right !== null && (big(right) < big(at) + BigInt(needle.length) || big(right) + BigInt(unit - 1) > big(q.end) || big(right) < big(from) + BigInt(data.length) || Number(big(right) - big(at) - BigInt(needle.length)) % unit || Number(big(right) - big(at) - BigInt(needle.length)) > q.cap - needle.length))
130
+ fail('partial', 'Invalid candidate terminator.');
131
+ const bounded = left !== null && right !== null, total = bounded ? Number(big(right) - big(left)) : null, text = decoded(data, encoding), complete = bounded && total <= q.cap && text !== null;
132
+ const reason = complete ? 'complete' : total !== null && total > q.cap || l.leftBoundary === 'string_limit' || l.rightBoundary === 'string_limit' ? 'string_limit' : !bounded ? 'missing_boundary' : 'invalid_encoding';
133
+ if (l.boundariesComplete !== bounded || l.byteLength !== total || l.text !== text || l.decoding !== (text === null ? 'invalid' : 'valid') || l.complete !== complete || l.reason !== reason || complete && (from !== left || data.length !== total))
134
+ fail('partial', 'Inconsistent string-anchor context/decoding facts.');
135
+ }
136
+ if (Buffer.byteLength(JSON.stringify(v)) > 1000000)
137
+ fail('limit', 'String-anchor native response exceeds its envelope reserve.');
138
+ return v;
139
+ }
140
+ export async function inspectAnchors(backend, who, r, view, observation, observe) {
141
+ if (r.expectedView !== undefined && r.expectedView !== view)
142
+ fail('binding', 'String-anchor continuation view changed.', { reason: 'changed_view' });
143
+ const modification = object(observation).modification;
144
+ if (typeof modification !== 'string')
145
+ fail('unsupported', 'String anchors require guarded native observations.');
146
+ const q = r.query;
147
+ let raw;
148
+ try {
149
+ raw = await backend.call(who, '/insight/string-anchors/v1', { text: q.text, encodings: q.encodings.join(','), scan_from: q.start, scan_through: q.end, resume_address: r.from.address, resume_encoding: r.from.encoding, max_string_bytes: q.cap, ...r.budgets });
150
+ }
151
+ catch (error) {
152
+ if (digest(observation) !== digest(await observe()))
153
+ fail('binding', 'Native view changed during string-anchor scan.', { reason: 'changed_view' });
154
+ throw error;
155
+ }
156
+ if (digest(observation) !== digest(await observe()))
157
+ fail('binding', 'Native view changed during string-anchor scan.', { reason: 'changed_view' });
158
+ const value = anchorResult(raw, r, modification), cursor = value.scan && object(value.scan).complete ? null : 'sa1.' + Buffer.from(stable({ version: 1, algorithm: anchorAlgorithm, scope: who, query: q, next: object(value.scan).next, view, budgets: r.budgets })).toString('base64url');
159
+ const result = { ...who, value: { ...value, view: { identity: view }, cursor } };
160
+ if (Buffer.byteLength(JSON.stringify(result)) > 1048576)
161
+ fail('limit', 'String-anchor response exceeds one MiB.');
162
+ return result;
163
+ }
164
+ export function stringAnchorCommands(research) {
165
+ return [command('strings', 'String research', [command('anchors', 'Find raw literal byte hits and separate bounded NUL-delimited candidate context', ({ options: o }, c) => { const who = scope(o); return research(c).stringAnchors(who, anchorRequest(o, who)); }, { permission: Permission.ReadOnly, options: [...selectedOptions,
166
+ { flags: '--text <literal>', description: 'Case-sensitive literal, 1–256 UTF-16 units, no NUL' },
167
+ { flags: '--encodings <encodings>', description: 'Explicit ascii, utf16le, or both; no alignment assumption' },
168
+ { flags: '--scan-from <address>', description: 'Inclusive range start; context stays inside the range' },
169
+ { flags: '--scan-through <address>', description: 'Inclusive range end; the whole needle must fit' },
170
+ { flags: '--max-string-bytes <count>', description: 'Even candidate/preview payload cap 16–8192, default 1024' },
171
+ { flags: '--cursor <token>', description: 'Stateless continuation; excludes text/encodings/range/context selectors' },
172
+ { flags: '--max-bytes <count>', description: '32768–16777216 actual bytes read including context/rereads; default 4194304' },
173
+ { flags: '--max-candidates <count>', description: '1–100000 raw matched address/encoding pairs; default 10000' },
174
+ { flags: '--timeout-seconds <count>', description: '1–30 second cooperative scan deadline; default 5' },
175
+ { flags: '--limit <count>', description: '1–1000 raw hits per call; default 100' }] })])];
176
+ }
@@ -40,6 +40,9 @@ export class Synchronization {
40
40
  async execute(selected, path, input, run) {
41
41
  const configured = this.workspace.context(selected).syncPolicy ?? manualPolicy;
42
42
  const caller = input.requestId === undefined ? undefined : id(input.requestId);
43
+ const recorded = caller ? this.workspace.scopedRequest(selected, caller) : undefined;
44
+ if (caller && recorded?.id === caller)
45
+ return run(caller);
43
46
  const previous = caller ? this.workspace.administrationByCaller(caller) : undefined;
44
47
  if (!previous && (configured.publish === 'manual' && configured.refresh === 'manual' || input.requestId !== undefined && this.workspace.requestByCaller(id(input.requestId))))
45
48
  return run(input.requestId === undefined ? '' : id(input.requestId));
@@ -0,0 +1,3 @@
1
+ import type { ResearchBackend } from './research.js';
2
+ /** Observe existing boundaries; retain original method receivers and domain behavior. */
3
+ export declare function timedBackend(backend: ResearchBackend): ResearchBackend;
@@ -0,0 +1,16 @@
1
+ import { timed } from './internal-timing.js';
2
+ /** Observe existing boundaries; retain original method receivers and domain behavior. */
3
+ export function timedBackend(backend) {
4
+ return new Proxy(backend, { get(target, key) {
5
+ const member = Reflect.get(target, key);
6
+ if (typeof member !== 'function')
7
+ return member;
8
+ if (key === 'open')
9
+ return (...args) => timed('context.open', 'open', async () => Reflect.apply(member, target, args));
10
+ if (key === 'saved')
11
+ return (...args) => timed('context.save', 'saved', async () => Reflect.apply(member, target, args));
12
+ if (key === 'call')
13
+ return (...args) => timed('backend.operation', String(args[1]), async () => Reflect.apply(member, target, args));
14
+ return member.bind(target);
15
+ } });
16
+ }
@@ -17,6 +17,7 @@ export declare class HttpBridge {
17
17
  private schema?;
18
18
  constructor(endpoint: string, fetch?: typeof globalThis.fetch, signal?: AbortSignal | undefined, token?: string | undefined, timeoutMilliseconds?: number | undefined, saveTimeoutMilliseconds?: number);
19
19
  private request;
20
+ private requestBody;
20
21
  instance(): Promise<Record<string, unknown>>;
21
22
  descriptor(path: string): Promise<Descriptor>;
22
23
  call(path: string, params?: Record<string, unknown>): Promise<unknown>;
@@ -1,5 +1,6 @@
1
1
  import { fail, object } from './model.js';
2
2
  import { parseDomainJson } from './json.js';
3
+ import { timed } from './internal-timing.js';
3
4
  import { CliError } from '@eyeauras/cli-factory';
4
5
  /** The pinned GhidraMCP HTTP side of a dedicated bridge. No discovery by project name. */
5
6
  export class HttpBridge {
@@ -26,6 +27,9 @@ export class HttpBridge {
26
27
  fail('invalid', 'Bridge timeout is outside the supported bound.');
27
28
  }
28
29
  async request(path, method, query, body) {
30
+ return timed('native.http', path, () => this.requestBody(path, method, query, body));
31
+ }
32
+ async requestBody(path, method, query, body) {
29
33
  const url = new URL(path, this.endpoint);
30
34
  // Import includes synchronous auto-analysis. Keep discovery/ordinary requests short,
31
35
  // while allowing a real executable to finish its first analysis without losing the reply.
@@ -197,9 +197,12 @@ export class Workspace {
197
197
  existingRequest(callerId, intentHash) {
198
198
  if (callerId === undefined)
199
199
  return;
200
- if (this.db.prepare('SELECT id FROM administration_requests WHERE caller_id=?').get(id(callerId)))
200
+ // A previous build may have reserved a blocked retry under this generated ID.
201
+ // The recorded identity still selects its original intent, not that newer caller ID.
202
+ const recorded = this.db.prepare('SELECT id,hash FROM requests WHERE id=?').get(id(callerId));
203
+ if (!recorded && this.db.prepare('SELECT id FROM administration_requests WHERE caller_id=?').get(id(callerId)))
201
204
  fail('idempotency', 'Request ID already identifies a profile administration operation.');
202
- const row = this.db.prepare('SELECT id,hash FROM requests WHERE caller_id=?').get(id(callerId));
205
+ const row = recorded ?? this.db.prepare('SELECT id,hash FROM requests WHERE caller_id=?').get(id(callerId));
203
206
  if (!row)
204
207
  return;
205
208
  if (row.hash !== intentHash)
package/package.json CHANGED
@@ -1,10 +1,21 @@
1
1
  {
2
2
  "name": "@eyeauras/insight-cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Agent-facing durable sessions and isolated Ghidra research contexts",
6
6
  "main": "./dist/src/index.js",
7
7
  "types": "./dist/src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/src/index.d.ts",
11
+ "default": "./dist/src/index.js"
12
+ },
13
+ "./persistent": {
14
+ "types": "./dist/src/persistent-client.d.ts",
15
+ "default": "./dist/src/persistent-client.js"
16
+ },
17
+ "./*": "./*"
18
+ },
8
19
  "bin": {
9
20
  "insight-cli": "./dist/src/bin.js"
10
21
  },
@@ -0,0 +1,135 @@
1
+ package com.eyeauras.insight;
2
+
3
+ import java.math.BigInteger;
4
+ import java.nio.ByteBuffer;
5
+ import java.nio.charset.*;
6
+ import java.util.*;
7
+ import java.util.function.LongSupplier;
8
+
9
+ /** Pure bounded raw-memory algorithm. No Ghidra, filesystem, process or persistent query state. */
10
+ public final class InsightAnchorScan {
11
+ public static final String ALGORITHM="raw-string-anchors-v1";
12
+ public interface Bytes { int read(BigInteger at,byte[] destination)throws Exception; }
13
+ public record Span(BigInteger start,BigInteger end) {}
14
+ public record Config(String text,List<String> encodings,BigInteger start,BigInteger end,
15
+ BigInteger resume,String resumeEncoding,int cap,int maxBytes,int maxCandidates,int seconds,int limit) {
16
+ public Config {
17
+ if(text==null||text.isEmpty()||text.length()>256||text.indexOf(0)>=0)throw new IllegalArgumentException("Expected 1–256 UTF-16 units without NUL");
18
+ for(int i=0;i<text.length();i++)if(Character.isSurrogate(text.charAt(i))){
19
+ if(!Character.isHighSurrogate(text.charAt(i))||i+1>=text.length()||!Character.isLowSurrogate(text.charAt(++i)))throw new IllegalArgumentException("Unpaired surrogate");
20
+ }
21
+ if(!List.of(List.of("ascii"),List.of("utf16le"),List.of("ascii","utf16le")).contains(encodings))throw new IllegalArgumentException("Expected canonical ascii,utf16le encodings");
22
+ if(encodings.contains("ascii")&&text.chars().anyMatch(c->c>127))throw new IllegalArgumentException("Text is not representable in ASCII");
23
+ if(start.signum()<0||end.bitLength()>64||start.compareTo(end)>0||resume.compareTo(start)<0||resume.compareTo(end)>0||!encodings.contains(resumeEncoding))throw new IllegalArgumentException("Invalid range/frontier");
24
+ if(cap<16||cap>8192||(cap&1)!=0||encodings.stream().anyMatch(e->encoded(text,e).length>cap))throw new IllegalArgumentException("Invalid candidate byte cap");
25
+ if(maxBytes<32768||maxBytes>16777216||maxCandidates<1||maxCandidates>100000||seconds<1||seconds>30||limit<1||limit>1000)throw new IllegalArgumentException("Invalid page budgets");
26
+ encodings=List.copyOf(encodings);
27
+ }
28
+ }
29
+ public static byte[] encoded(String text,String encoding){return text.getBytes(encoding.equals("ascii")?StandardCharsets.US_ASCII:StandardCharsets.UTF_16LE);}
30
+ private static final BigInteger ONE=BigInteger.ONE,PAGE=BigInteger.valueOf(4096);
31
+ private static String hex(BigInteger at){return at.toString(16);}
32
+ private static Map<String,Object> frontier(BigInteger at,String encoding){return Map.of("address",hex(at),"encoding",encoding);}
33
+ private static final class Pause extends RuntimeException {final String reason;Pause(String reason){this.reason=reason;}}
34
+ private record Page(BigInteger start,BigInteger end,byte[] bytes) {}
35
+ private record Probe(int bytes,String boundary,BigInteger nul) {}
36
+ private final Config c;private final List<Span> spans;private final Bytes source;private final LongSupplier clock;
37
+ private final long started;private final byte[][] needles;private final LinkedHashMap<BigInteger,Page> cache=new LinkedHashMap<>(16,.75f,true);
38
+ private Page last,positionPage;private int positionIndex,encodingIndex,bytesRead,testedPairs,candidates,estimatedResultBytes=2048;
39
+ private BigInteger position;private final List<Object> matches=new ArrayList<>();
40
+ public InsightAnchorScan(Config config,List<Span> spans,Bytes source,LongSupplier clock){
41
+ c=config;this.spans=List.copyOf(spans);this.source=source;this.clock=clock;started=clock.getAsLong();
42
+ needles=c.encodings.stream().map(e->encoded(c.text,e)).toArray(byte[][]::new);position=c.resume;encodingIndex=c.encodings.indexOf(c.resumeEncoding);
43
+ BigInteger previous=null;
44
+ for(Span s:spans){if(s.start.compareTo(c.start)<0||s.end.compareTo(c.end)>0||s.start.compareTo(s.end)>0||previous!=null&&s.start.compareTo(previous.add(ONE))<=0)throw new IllegalArgumentException("Expected clipped, sorted, merged spans");previous=s.end;}
45
+ }
46
+ private void deadline(){if(clock.getAsLong()-started>=c.seconds*1000000000L)throw new Pause("timeout");}
47
+ private BigInteger at(){return positionPage==null?position:positionPage.start.add(BigInteger.valueOf(positionIndex));}
48
+ private Page page(BigInteger at,Span span)throws Exception{
49
+ if(last!=null&&at.compareTo(last.start)>=0&&at.compareTo(last.end)<=0)return last;
50
+ BigInteger base=at.divide(PAGE).multiply(PAGE),start=base.max(span.start),end=base.add(PAGE).subtract(ONE).min(span.end);
51
+ Page hit=cache.get(start);if(hit!=null)return last=hit;
52
+ deadline();int length=end.subtract(start).intValueExact()+1;
53
+ if(length>c.maxBytes-bytesRead)throw new Pause("byte_limit");
54
+ byte[] data=new byte[length];int read=source.read(start,data);if(read!=length)throw new IllegalStateException("Incomplete initialized-memory read");
55
+ bytesRead+=read;Page value=new Page(start,end,data);cache.put(start,value);if(cache.size()>8)cache.remove(cache.keySet().iterator().next());return last=value;
56
+ }
57
+ private int byteAt(BigInteger at,Span span)throws Exception{Page p=page(at,span);return p.bytes[at.subtract(p.start).intValueExact()]&255;}
58
+ private byte[] copy(BigInteger at,int count,Span span)throws Exception{
59
+ byte[] data=new byte[count];int offset=0;
60
+ while(offset<count){Page p=page(at,span);int index=at.subtract(p.start).intValueExact(),n=Math.min(count-offset,p.bytes.length-index);System.arraycopy(p.bytes,index,data,offset,n);offset+=n;at=at.add(BigInteger.valueOf(n));}
61
+ return data;
62
+ }
63
+ private int unit(BigInteger at,int width,Span span)throws Exception{return width==1?byteAt(at,span):byteAt(at,span)|(byteAt(at.add(ONE),span)<<8);}
64
+ private Probe probe(BigInteger match,int length,int width,Span span,boolean left)throws Exception{
65
+ int count=0,allowance=c.cap-length;BigInteger cursor=left?match.subtract(BigInteger.valueOf(width)):match.add(BigInteger.valueOf(length));
66
+ while(true){
67
+ if((count&127)==0)deadline();
68
+ if(cursor.compareTo(span.start)<0||cursor.add(BigInteger.valueOf(width-1)).compareTo(span.end)>0){
69
+ boolean range=left?cursor.compareTo(c.start)<0:cursor.add(BigInteger.valueOf(width-1)).compareTo(c.end)>0;
70
+ return new Probe(count,range?"range_edge":"memory_edge",null);
71
+ }
72
+ // At the payload cap the next code unit may still be the NUL (not payload).
73
+ if(unit(cursor,width,span)==0)return new Probe(count,"nul",cursor);
74
+ if(count==allowance)return new Probe(count,"string_limit",null);
75
+ count+=width;cursor=left?cursor.subtract(BigInteger.valueOf(width)):cursor.add(BigInteger.valueOf(width));
76
+ }
77
+ }
78
+ private Map<String,Object> candidate(BigInteger match,int index,Span span)throws Exception{
79
+ String encoding=c.encodings.get(index);int width=encoding.equals("ascii")?1:2,length=needles[index].length;
80
+ Probe left=probe(match,length,width,span,true),right=probe(match,length,width,span,false);
81
+ int spare=c.cap-length,l=Math.min(left.bytes,(spare/2/width)*width),r=Math.min(right.bytes,spare-l);
82
+ l+=Math.min(left.bytes-l,spare-l-r);r+=Math.min(right.bytes-r,spare-l-r);
83
+ BigInteger textFrom=match.subtract(BigInteger.valueOf(l));byte[] data=copy(textFrom,l+length+r,span);String text=null;
84
+ try{text=(encoding.equals("ascii")?StandardCharsets.US_ASCII:StandardCharsets.UTF_16LE).newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT).decode(ByteBuffer.wrap(data)).toString();}catch(CharacterCodingException ignored){}
85
+ boolean bounded=left.nul!=null&&right.nul!=null;int total=left.bytes+length+right.bytes;
86
+ boolean complete=bounded&&total<=c.cap&&text!=null;
87
+ String reason=complete?"complete":bounded&&total>c.cap||left.boundary.equals("string_limit")||right.boundary.equals("string_limit")?"string_limit":!bounded?"missing_boundary":"invalid_encoding";
88
+ Map<String,Object> literal=new LinkedHashMap<>();literal.put("start",left.nul==null?null:hex(left.nul.add(BigInteger.valueOf(width))));literal.put("terminatorAddress",right.nul==null?null:hex(right.nul));
89
+ literal.put("byteLength",bounded?total:null);literal.put("textFrom",hex(textFrom));literal.put("text",text);literal.put("textBytes",HexFormat.of().formatHex(data));literal.put("textByteLength",data.length);
90
+ literal.put("leftBoundary",left.boundary);literal.put("rightBoundary",right.boundary);literal.put("boundariesComplete",bounded);literal.put("decoding",text==null?"invalid":"valid");literal.put("complete",complete);literal.put("reason",reason);
91
+ Map<String,Object> row=new LinkedHashMap<>();row.put("encoding",encoding);row.put("matchAddress",hex(match));row.put("matchByteLength",length);row.put("matchOffsetBytes",left.nul==null?null:left.bytes);row.put("literalCandidate",literal);return row;
92
+ }
93
+ private boolean match(Page p,int index,int which,Span span)throws Exception{
94
+ byte[] needle=needles[which];if(p.bytes[index]!=needle[0])return false;
95
+ BigInteger at=p.start.add(BigInteger.valueOf(index));if(at.add(BigInteger.valueOf(needle.length-1)).compareTo(span.end)>0)return false;
96
+ if(index+needle.length<=p.bytes.length){for(int i=1;i<needle.length;i++)if(p.bytes[index+i]!=needle[i])return false;return true;}
97
+ for(int i=1;i<needle.length;i++){if((i&63)==0)deadline();if(byteAt(at.add(BigInteger.valueOf(i)),span)!=(needle[i]&255))return false;}return true;
98
+ }
99
+ public Map<String,Object> run()throws Exception{
100
+ String reason="exhausted";boolean complete=true;
101
+ try{
102
+ for(Span span:spans){
103
+ if(span.end.compareTo(c.resume)<0)continue;
104
+ BigInteger current=span.start.max(c.resume);int first=current.equals(c.resume)?c.encodings.indexOf(c.resumeEncoding):0;
105
+ while(current.compareTo(span.end)<=0){
106
+ position=current;positionPage=null;encodingIndex=first;
107
+ // Limits apply only while there remains an eligible unchecked pair.
108
+ if(matches.size()>=c.limit||estimatedResultBytes>=900000)throw new Pause("result_limit");
109
+ if(candidates>=c.maxCandidates)throw new Pause("candidate_limit");
110
+ Page p=page(current,span);positionPage=p;
111
+ for(int i=current.subtract(p.start).intValueExact();i<p.bytes.length;i++){
112
+ positionIndex=i;
113
+ for(int e=first;e<needles.length;e++){
114
+ encodingIndex=e;if((testedPairs&1023)==0)deadline();
115
+ if(matches.size()>=c.limit||estimatedResultBytes>=900000)throw new Pause("result_limit");
116
+ if(candidates>=c.maxCandidates)throw new Pause("candidate_limit");
117
+ if(match(p,i,e,span)){
118
+ candidates++;Map<String,Object> row=candidate(at(),e,span);
119
+ Map<?,?> literal=(Map<?,?>)row.get("literalCandidate");int cost=2048+8*((String)literal.get("textBytes")).length();
120
+ if(estimatedResultBytes+cost>900000)throw new Pause("result_limit");
121
+ matches.add(row);estimatedResultBytes+=cost;
122
+ }
123
+ testedPairs++;
124
+ }
125
+ first=0;
126
+ }
127
+ current=p.end.add(ONE);first=0;
128
+ }
129
+ }
130
+ }catch(Pause pause){complete=false;reason=pause.reason;}
131
+ Map<String,Object> scan=new LinkedHashMap<>();scan.put("from",frontier(c.resume,c.resumeEncoding));scan.put("through",hex(c.end));scan.put("next",complete?null:frontier(at(),c.encodings.get(encodingIndex)));
132
+ scan.put("complete",complete);scan.put("reason",reason);scan.put("bytesRead",bytesRead);scan.put("testedPairs",testedPairs);scan.put("candidates",candidates);scan.put("elapsedMs",Math.max(0,(clock.getAsLong()-started)/1000000.0));
133
+ return Map.of("algorithm",ALGORITHM,"encodings",c.encodings,"matches",matches,"scan",scan);
134
+ }
135
+ }
@@ -0,0 +1,129 @@
1
+ import ghidra.app.script.GhidraScript;
2
+ import ghidra.app.util.PseudoDisassembler;
3
+ import ghidra.app.util.PseudoInstruction;
4
+ import ghidra.program.model.address.*;
5
+ import ghidra.program.model.mem.MemoryBlock;
6
+ import com.google.gson.*;
7
+ import java.nio.charset.StandardCharsets;
8
+ import java.util.*;
9
+
10
+ /** Bounded candidate discovery only. Never creates instructions, functions or references. */
11
+ public class InsightCodeReferences extends GhidraScript {
12
+ static final String ALGORITHM="x86-64-code-reference-candidates-v1";
13
+ static final List<String> FORMS=List.of("call-rel32","lea-rip","mov-rip","call-rip-slot");
14
+ JsonObject p; long deadline,started; int bytesRead,tested,visited,maxBytes,maxCandidates,limit;
15
+ String reason="exhausted"; Address next; Set<Address> targets=new HashSet<>(); List<String> forms=new ArrayList<>();
16
+ List<Object> matches=new ArrayList<>();
17
+ Map<String,Object> map(Object... pairs){Map<String,Object> m=new LinkedHashMap<>();for(int i=0;i<pairs.length;i+=2)m.put((String)pairs[i],pairs[i+1]);return m;}
18
+ int number(String k,int d,int min,int max){int v=p.has(k)?p.get(k).getAsInt():d;if(v<min||v>max)throw new IllegalArgumentException(k+" outside supported bounds");return v;}
19
+ Address address(String s){if(!s.matches("[a-fA-F0-9]{1,16}"))throw new IllegalArgumentException("Default-space 64-bit hexadecimal address required");return currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(Long.parseUnsignedLong(s,16));}
20
+ String hex(Address a){return Long.toUnsignedString(a.getOffset(),16);}
21
+ boolean expired()throws Exception{monitor.checkCancelled();return System.nanoTime()>=deadline;}
22
+ record Encoding(String form,int length,int operand,int bits,int delta,String role){}
23
+ Encoding encoding(byte[] b,int start){
24
+ int i=start,rex=0;boolean word=false;
25
+ while(i<b.length&&i-start<15){int v=b[i]&255;if(v==0x66||v==0x26||v==0x2e||v==0x36||v==0x3e){word|=v==0x66;rex=0;i++;}else if(v>=0x40&&v<=0x4f){rex=v;i++;}else break;}
26
+ if(i>=b.length)return null;int op=b[i]&255,length,disp,operand,bits;String form,role;
27
+ if(op==0xe8){form="call-rel32";role="direct-call-target";length=i-start+5;disp=i+1;operand=0;bits=64;}
28
+ else{
29
+ if(i+1>=b.length)return null;int m=b[i+1]&255;if((m&0xc7)!=5)return null;
30
+ if(op==0x8d){form="lea-rip";operand=1;}
31
+ else if(op==0x88||op==0x89||op==0x8a||op==0x8b){form="mov-rip";operand=op==0x88||op==0x89?0:1;}
32
+ else if(op==0xff&&((m>>3)&7)==2){form="call-rip-slot";operand=0;}
33
+ else return null;
34
+ role=form.equals("call-rip-slot")?"pointer-slot":"effective-address";
35
+ length=i-start+6;disp=i+2;bits=form.equals("call-rip-slot")?64:op==0x88||op==0x8a?8:(rex&8)!=0?64:word?16:32;
36
+ }
37
+ if(length>15||start+length>b.length)return null;
38
+ int delta=(b[disp]&255)|((b[disp+1]&255)<<8)|((b[disp+2]&255)<<16)|(b[disp+3]<<24);
39
+ return new Encoding(form,length,operand,bits,delta,role);
40
+ }
41
+ boolean compatible(PseudoInstruction i,Encoding e,Address target){
42
+ if(i==null||i.getLength()!=e.length)return false;
43
+ if(e.form.equals("call-rel32"))return i.getMnemonicString().equalsIgnoreCase("CALL")&&i.getFlowType().isCall()&&!i.getFlowType().isComputed()&&Arrays.asList(i.getFlows()).contains(target);
44
+ if(e.form.equals("call-rip-slot"))return i.getMnemonicString().equalsIgnoreCase("CALL")&&i.getFlowType().isCall()&&i.getFlowType().isComputed();
45
+ if(!i.getMnemonicString().equalsIgnoreCase(e.form.equals("lea-rip")?"LEA":"MOV"))return false;
46
+ var register=i.getRegister(e.operand==0?1:0);return register!=null&&register.getBitLength()==e.bits;
47
+ }
48
+ String boundary(Address at,PseudoInstruction decoded,byte[] bytes)throws Exception{
49
+ var listing=currentProgram.getListing();var stored=listing.getInstructionContaining(at);
50
+ if(stored==null){var after=listing.getInstructionAfter(at);return after!=null&&after.getAddress().compareTo(at.addNoWrap(bytes.length-1))<=0?"listing-conflict":"unconfirmed";}
51
+ if(!stored.getAddress().equals(at)||stored.getLength()!=decoded.getLength()||!Arrays.equals(stored.getBytes(),bytes)||!stored.getMnemonicString().equals(decoded.getMnemonicString())||stored.getNumOperands()!=decoded.getNumOperands())return "listing-conflict";
52
+ for(int i=0;i<stored.getNumOperands();i++)if(!stored.getDefaultOperandRepresentation(i).equals(decoded.getDefaultOperandRepresentation(i)))return "listing-conflict";
53
+ return "listing-confirmed";
54
+ }
55
+ Object scan()throws Exception{
56
+ if(!currentProgram.getLanguageID().toString().startsWith("x86:LE:64:")||currentProgram.getAddressFactory().getDefaultAddressSpace().getSize()!=64)throw new UnsupportedOperationException("Code references V1 requires x86-64 little-endian default memory space");
57
+ var mode=currentProgram.getRegister("longMode");if(mode==null)throw new UnsupportedOperationException("64-bit decode mode is unavailable");
58
+ Address from=address(p.get("scan_from").getAsString()),through=address(p.get("scan_through").getAsString());if(from.compareTo(through)>0)throw new IllegalArgumentException("Inverted scan range");
59
+ var ts=p.getAsJsonArray("targets");if(ts.size()<1||ts.size()>64)throw new IllegalArgumentException("Select 1..64 targets");for(var t:ts)if(!targets.add(address(t.getAsString())))throw new IllegalArgumentException("Duplicate target");
60
+ var fs=p.getAsJsonArray("forms");for(String f:FORMS)for(var v:fs)if(f.equals(v.getAsString()))forms.add(f);if(forms.isEmpty()||new HashSet<>(forms).size()!=fs.size()||forms.size()!=fs.size())throw new IllegalArgumentException("Unsupported or duplicate form");
61
+ maxBytes=number("max_bytes",4194304,15,16777216);maxCandidates=number("max_candidates",200000,1,1000000);limit=number("limit",100,1,1000);
62
+ started=System.nanoTime();deadline=started+number("timeout_seconds",5,1,30)*1000000000L;
63
+ List<Address[]> runs=new ArrayList<>();var blocks=currentProgram.getMemory().getBlocks();Arrays.sort(blocks,Comparator.comparing(MemoryBlock::getStart));
64
+ for(var block:blocks){
65
+ if(expired()){reason="timeout";next=from;return result(from,through);}
66
+ if(!block.isInitialized()||!block.isExecute()||!block.getStart().getAddressSpace().equals(from.getAddressSpace())||block.getEnd().compareTo(from)<0||block.getStart().compareTo(through)>0)continue;
67
+ Address a=block.getStart().compareTo(from)<0?from:block.getStart(),z=block.getEnd().compareTo(through)>0?through:block.getEnd();
68
+ if(!runs.isEmpty()&&a.equals(runs.getLast()[1].next()))runs.getLast()[1]=z;
69
+ else runs.add(new Address[]{a,z});
70
+ }
71
+ var decoder=new PseudoDisassembler(currentProgram);
72
+ outer:for(int runIndex=0;runIndex<runs.size();runIndex++){
73
+ var run=runs.get(runIndex);Address at=run[0];
74
+ while(at.compareTo(run[1])<=0){
75
+ next=at;
76
+ if(expired()){reason="timeout";break outer;}
77
+ int remaining=maxBytes-bytesRead;long available=run[1].subtract(at)+1;
78
+ if(remaining<Math.min(15,available)){reason="byte_limit";break outer;}
79
+ int count=(int)Math.min(Math.min(65550,available),remaining);byte[] data=new byte[count];
80
+ if(currentProgram.getMemory().getBytes(at,data)!=count)throw new IllegalStateException("Incomplete initialized executable memory read");bytesRead+=count;
81
+ int positions=count==available?count:count-14;
82
+ for(int i=0;i<positions;i++){
83
+ Address here=at.addNoWrap(i);next=here;
84
+ if(expired()){reason="timeout";break outer;}
85
+ Encoding e=encoding(data,i);
86
+ if(e!=null&&forms.contains(e.form)){
87
+ if(visited==maxCandidates){reason="candidate_limit";break outer;}
88
+ var modeValue=currentProgram.getProgramContext().getValue(mode,here,false);
89
+ if(modeValue==null||!modeValue.equals(java.math.BigInteger.ONE))throw new UnsupportedOperationException("Candidate encountered outside known 64-bit decode mode");
90
+ visited++;
91
+ Address target=here.addWrap(e.length+(long)e.delta);
92
+ if(targets.contains(target)){
93
+ PseudoInstruction instruction=null;
94
+ try{instruction=decoder.disassemble(here,Arrays.copyOfRange(data,i,Math.min(data.length,i+15)));}
95
+ catch(ghidra.program.model.lang.InsufficientBytesException|ghidra.program.model.lang.UnknownInstructionException ignored){}
96
+ catch(ghidra.program.model.lang.UnknownContextException error){throw new UnsupportedOperationException("Native decoder context unavailable");}
97
+ if(compatible(instruction,e,target)){
98
+ byte[] bytes=Arrays.copyOfRange(data,i,i+e.length);String display=instruction.toString();
99
+ matches.add(map("from",hex(here),"to",hex(target),"form",e.form,"bytes",HexFormat.of().formatHex(bytes),"length",e.length,"operand_index",e.operand,"operand_bits",e.bits,"target_role",e.role,"validation","decoded","boundary",boundary(here,instruction,bytes),"instruction",display.substring(0,Math.min(512,display.length()))));
100
+ }
101
+ }
102
+ }
103
+ tested++;
104
+ if(here.equals(through)||here.equals(run[1])&&runIndex==runs.size()-1)next=null;
105
+ else next=here.addNoWrap(1);
106
+ if(matches.size()==limit&&next!=null){reason="result_limit";break outer;}
107
+ }
108
+ if(positions==available)break;
109
+ at=at.addNoWrap(positions);
110
+ }
111
+ }
112
+ if(reason.equals("exhausted"))next=null;
113
+ return result(from,through);
114
+ }
115
+ Object result(Address from,Address through){return map("algorithm",ALGORITHM,"language",currentProgram.getLanguageID().toString(),"forms",forms,"matches",matches,
116
+ "scan",map("from",hex(from),"through",hex(through),"next_start",next==null?null:hex(next),"complete",reason.equals("exhausted"),"reason",reason,"bytes_read",bytesRead,"tested_starts",tested,"visited_candidates",visited,"elapsed_ms",(System.nanoTime()-started)/1000000.0),
117
+ "coverage","Every byte start in initialized executable default-space memory, in address order; adjacent eligible blocks merge. Instructions fit entirely within the requested range. Overlapping decodes are separate candidates.",
118
+ "limitations",List.of("Decoded byte candidates do not prove instruction boundaries, execution or callgraph membership.","Indirect CALL targets identify pointer slots; pointees are never followed.","Only explicit V1 encodings; 67, FS/GS, LOCK/REP, SIMD and immediate MOV encodings are excluded at that start. A later start inside their bytes may still be a candidate."));}
119
+ public void run()throws Exception{
120
+ Map<String,Object> output;long modification=currentProgram.getModificationNumber();
121
+ try{
122
+ var request=JsonParser.parseString(new String(Base64.getDecoder().decode(getScriptArgs()[0]),StandardCharsets.UTF_8)).getAsJsonObject();
123
+ if(!request.get("operation").getAsString().equals("/code_references"))throw new IllegalArgumentException("Unknown code-reference operation");p=request.getAsJsonObject("parameters");
124
+ var value=(Map<String,Object>)scan();if(currentProgram.getModificationNumber()!=modification)throw new IllegalStateException("Read-only scan changed program modification counter");
125
+ value.put("read_only",map("modification_unchanged",true,"modification",Long.toString(modification)));output=map("value",value);
126
+ }catch(Exception error){output=map("error",map("code",error instanceof UnsupportedOperationException?"unsupported":error instanceof IllegalArgumentException?"invalid":"partial","message",Objects.toString(error.getMessage(),"Native code-reference scan failed")));}
127
+ println("INSIGHT_QUERY:"+new GsonBuilder().serializeNulls().create().toJson(output));
128
+ }
129
+ }
@@ -0,0 +1,56 @@
1
+ package com.eyeauras.insight;
2
+
3
+ import com.xebyte.core.*;
4
+ import com.google.gson.Gson;
5
+ import com.google.gson.GsonBuilder;
6
+ import ghidra.program.model.listing.Program;
7
+ import java.math.BigInteger;
8
+ import java.util.*;
9
+
10
+ /** Authenticated bounded runtime-region evidence for explicitly selected open programs. */
11
+ public final class InsightFunctionBoundaries {
12
+ private static final Gson JSON=new GsonBuilder().disableHtmlEscaping().serializeNulls().create();
13
+ private final ProgramProvider programs;private final ThreadingStrategy threading;
14
+ public InsightFunctionBoundaries(ProgramProvider programs,ThreadingStrategy threading){this.programs=programs;this.threading=threading;}
15
+ @McpTool(path="/insight/function-boundaries/v1",method="GET",description="Compare recorded function membership with current mapped PE32+ AMD64 runtime regions; no semantic reconstruction",access=ToolAccess.READ_ONLY)
16
+ public Response boundaries(@Param(value="program",source=ParamSource.QUERY,allowEmpty=false) String path,
17
+ @Param(value="addresses",source=ParamSource.QUERY,allowEmpty=false) String addresses,
18
+ @Param(value="max_unwind_bytes",source=ParamSource.QUERY,allowEmpty=false) int maximum)throws Exception {
19
+ if(maximum<1||maximum>16777216||addresses==null||addresses.length()>339)throw new IllegalArgumentException("Expected 1–20 addresses and byte budget 1–16777216");
20
+ String[] words=addresses.split(",",-1);if(words.length<1||words.length>20)throw new IllegalArgumentException("Expected 1–20 addresses");
21
+ Set<String> unique=new HashSet<>();for(String word:words)if(!word.matches("0|[1-9a-f][a-f0-9]{0,15}")||!unique.add(word))throw new IllegalArgumentException("Expected distinct canonical addresses");
22
+ return threading.executeRead(()->{
23
+ if(programs==null||path==null||!path.startsWith("/")||path.length()>4096)throw new IllegalArgumentException("Explicit open program path required");
24
+ Program program=null;for(Program p:programs.getAllOpenPrograms())if(p!=null&&!p.isClosed()&&p.getDomainFile()!=null&&path.equals(p.getDomainFile().getPathname())){if(program!=null&&program!=p)throw new IllegalStateException("Ambiguous program path");program=p;}
25
+ if(program==null)throw new IllegalArgumentException("Program is not open");
26
+ var space=program.getAddressFactory().getDefaultAddressSpace();var memory=program.getMemory();var functions=program.getFunctionManager();long modification=program.getModificationNumber();
27
+ long lookupStarted=System.nanoTime();List<InsightPeRanges.Span> spans=new ArrayList<>();
28
+ for(var block:memory.getBlocks())if(block.isInitialized()&&block.getStart().getAddressSpace().equals(space))spans.add(new InsightPeRanges.Span(new BigInteger(Long.toUnsignedString(block.getStart().getOffset(),16),16),new BigInteger(Long.toUnsignedString(block.getEnd().getOffset(),16),16).add(BigInteger.ONE),block.isExecute()));
29
+ InsightPeRanges.Result scan;
30
+ if(space.getSize()!=64||space.getAddressableUnitSize()!=1||!program.getLanguageID().toString().equals("x86:LE:64:default"))scan=new InsightPeRanges.Result(Map.of("state","unavailable","complete",false,"reason","unsupported_language","headersSource","current-mapped-memory","recordCount",0,"recordsValidated",0,"bytesRead",0,"maximumBytes",maximum,"deadlineMilliseconds",5000),List.of());
31
+ else scan=new InsightPeRanges(new BigInteger(Long.toUnsignedString(program.getImageBase().getOffset(),16),16),spans,(at,out)->memory.getBytes(space.getAddress(at.longValue()),out),maximum,System::nanoTime,Arrays.stream(words).map(w->new BigInteger(w,16)).toList(),lookupStarted).run();
32
+ Map<String,Object> items=new LinkedHashMap<>();boolean complete=Boolean.TRUE.equals(scan.lookup().get("complete"));
33
+ for(String word:words){
34
+ BigInteger query=new BigInteger(word,16);Map<String,Object> row=new LinkedHashMap<>();Map<String,Object> database=null;
35
+ if(query.bitLength()<=space.getSize()){
36
+ var at=space.getAddress(query.longValue());var f=functions.getFunctionAt(at);String kind="exact-entry";if(f==null){f=functions.getFunctionContaining(at);kind="recorded-containing";}
37
+ if(f!=null)database=Map.of("entryPoint",Long.toUnsignedString(f.getEntryPoint().getOffset(),16),"kind",kind,"recordedBodyContainsAddress",f.getBody().contains(at));
38
+ }
39
+ row.put("database",database);InsightPeRanges.Range range=null;
40
+ for(var candidate:scan.ranges())if(query.compareTo(candidate.begin())>=0&&query.compareTo(candidate.endExclusive())<0){range=candidate;break;}
41
+ row.put("boundary",range==null?null:range.facts());row.put("status","unresolved");
42
+ if(!complete)row.put("reason","boundary_lookup_"+scan.lookup().get("state"));
43
+ else if(range!=null&&database!=null&&!database.get("entryPoint").equals(range.begin().toString(16)))row.put("reason","boundary_conflict");
44
+ else if(range!=null){
45
+ String entry=range.begin().toString(16);
46
+ if(functions.getFunctionAt(space.getAddress(range.begin().longValue()))==null)row.put("reason","runtime_entry_not_defined");
47
+ else{row.put("status","resolved");row.put("entryPoint",entry);row.put("kind","pe-runtime-range");row.put("recordedBodyContainsAddress",database!=null&&Boolean.TRUE.equals(database.get("recordedBodyContainsAddress")));}
48
+ }else if(database!=null){row.put("status","resolved");row.put("entryPoint",database.get("entryPoint"));row.put("kind",database.get("kind"));row.put("recordedBodyContainsAddress",database.get("recordedBodyContainsAddress"));}
49
+ else row.put("reason","no_runtime_range_or_recorded_function");
50
+ items.put(word,row);
51
+ }
52
+ if(modification!=program.getModificationNumber())throw new IllegalStateException("Program changed during function-boundary lookup");
53
+ return Response.text(JSON.toJson(Map.of("protocol","insight-function-boundaries-v1","modification",Long.toString(modification),"boundaryLookup",scan.lookup(),"items",items)));
54
+ });
55
+ }
56
+ }