@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.
- package/.cli-package-build.json +1 -1
- package/SKILL.md +4 -1
- package/dist/src/analysis-default.d.ts +26 -0
- package/dist/src/analysis-default.js +149 -0
- package/dist/src/analysis-storage.d.ts +39 -20
- package/dist/src/analysis-storage.js +112 -16
- package/dist/src/arguments.js +9 -4
- package/dist/src/cli.js +45 -37
- package/dist/src/code-references.d.ts +48 -0
- package/dist/src/code-references.js +154 -0
- package/dist/src/context-diagnostics.d.ts +25 -0
- package/dist/src/context-diagnostics.js +82 -0
- package/dist/src/contexts.d.ts +13 -1
- package/dist/src/contexts.js +38 -9
- package/dist/src/function-boundaries.d.ts +9 -0
- package/dist/src/function-boundaries.js +68 -0
- package/dist/src/function-entries.d.ts +33 -0
- package/dist/src/function-entries.js +84 -0
- package/dist/src/function-inspection.d.ts +4 -2
- package/dist/src/function-inspection.js +94 -59
- package/dist/src/function-results.d.ts +3 -1
- package/dist/src/function-results.js +9 -3
- package/dist/src/inline-script.d.ts +15 -0
- package/dist/src/inline-script.js +62 -0
- package/dist/src/internal-timing.d.ts +3 -0
- package/dist/src/internal-timing.js +52 -0
- package/dist/src/lifecycle.js +14 -4
- package/dist/src/native-inputs.d.ts +1 -1
- package/dist/src/native-inputs.js +1 -1
- package/dist/src/native-queries.js +1 -1
- package/dist/src/persistent-client.d.ts +30 -0
- package/dist/src/persistent-client.js +125 -0
- package/dist/src/queries.js +4 -1
- package/dist/src/query-policy.js +9 -1
- package/dist/src/research-view.d.ts +6 -0
- package/dist/src/research-view.js +9 -0
- package/dist/src/research.d.ts +38 -0
- package/dist/src/research.js +78 -8
- package/dist/src/string-anchors.d.ts +43 -0
- package/dist/src/string-anchors.js +176 -0
- package/dist/src/sync-policy.js +3 -0
- package/dist/src/timed-backend.d.ts +3 -0
- package/dist/src/timed-backend.js +16 -0
- package/dist/src/transport.d.ts +1 -0
- package/dist/src/transport.js +4 -0
- package/dist/src/workspace.js +5 -2
- package/package.json +12 -1
- package/references/native/InsightAnchorScan.java +135 -0
- package/references/native/InsightCodeReferences.java +129 -0
- package/references/native/InsightFunctionBoundaries.java +56 -0
- package/references/native/InsightInlineExecutor.java +143 -0
- package/references/native/InsightInlineScript.java +14 -0
- package/references/native/InsightPeRanges.java +92 -0
- package/references/native/InsightResearchQueries.java +22 -1
- package/references/native/InsightResearchView.java +69 -0
- package/references/native/InsightStringAnchors.java +49 -0
- package/references/persistent-client.mjs +125 -0
- package/references/persistent.md +50 -0
- package/references/query-semantics.md +12 -0
- package/references/recovery.md +36 -0
- package/references/research.md +87 -0
- package/references/setup.md +1 -1
- package/references/sparse-navigation.md +173 -0
- package/references/storage.md +35 -7
- package/runtime/GhidraMCP-7.0.0.jar +0 -0
- package/runtime/GhidraMCP-NOTICE +5 -0
- package/runtime/provenance.json +17 -3
- package/runtime/research-view.provenance.json +28 -0
- package/runtime/toolbox.json +10 -3
- package/skill-bundle/insight-artifacts/SKILL.md +1 -1
- package/skill-bundle/insight-artifacts/references/recovery.md +36 -0
- package/skill-bundle/insight-artifacts/references/storage.md +35 -7
- package/skill-bundle/insight-binary/SKILL.md +1 -1
- package/skill-bundle/insight-capture/SKILL.md +1 -1
- package/skill-bundle/insight-cli/SKILL.md +4 -1
- package/skill-bundle/insight-cli/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-cli/references/persistent.md +50 -0
- package/skill-bundle/insight-cli/references/recovery.md +36 -0
- package/skill-bundle/insight-cli/references/setup.md +1 -1
- package/skill-bundle/insight-cli/references/storage.md +35 -7
- package/skill-bundle/insight-die/SKILL.md +1 -1
- package/skill-bundle/insight-dnspy/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-dump/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-stack/SKILL.md +1 -1
- package/skill-bundle/insight-dotnet-trace/SKILL.md +1 -1
- package/skill-bundle/insight-frida/SKILL.md +1 -1
- package/skill-bundle/insight-ghidra/SKILL.md +14 -1
- package/skill-bundle/insight-ghidra/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-ghidra/references/persistent.md +50 -0
- package/skill-bundle/insight-ghidra/references/query-semantics.md +12 -0
- package/skill-bundle/insight-ghidra/references/recovery.md +36 -0
- package/skill-bundle/insight-ghidra/references/research.md +87 -0
- package/skill-bundle/insight-ghidra/references/setup.md +1 -1
- package/skill-bundle/insight-ghidra/references/sparse-navigation.md +173 -0
- package/skill-bundle/insight-ghidra/references/storage.md +35 -7
- package/skill-bundle/insight-ilspy/SKILL.md +1 -1
- package/skill-bundle/insight-interop/SKILL.md +1 -1
- package/skill-bundle/insight-lldb/SKILL.md +1 -1
- package/skill-bundle/insight-llvm/SKILL.md +1 -1
- package/skill-bundle/insight-managed-heap/SKILL.md +1 -1
- package/skill-bundle/insight-managed-metadata/SKILL.md +1 -1
- package/skill-bundle/insight-mitmproxy/SKILL.md +1 -1
- package/skill-bundle/insight-overview/SKILL.md +1 -1
- package/skill-bundle/insight-procmon/SKILL.md +1 -1
- package/skill-bundle/insight-re/SKILL.md +4 -1
- package/skill-bundle/insight-re/references/persistent-client.mjs +125 -0
- package/skill-bundle/insight-re/references/persistent.md +50 -0
- package/skill-bundle/insight-re/references/recovery.md +36 -0
- package/skill-bundle/insight-re/references/storage.md +35 -7
- package/skill-bundle/insight-sevenzip/SKILL.md +1 -1
- package/skill-bundle/insight-strace/SKILL.md +1 -1
- package/skill-bundle/insight-tshark/SKILL.md +1 -1
- package/skill-bundle/insight-windbg/SKILL.md +1 -1
- package/skill-bundle/manifest.json +1 -1
- package/skills/insight-artifacts/SKILL.md +1 -1
- package/skills/insight-artifacts/references/recovery.md +36 -0
- package/skills/insight-artifacts/references/storage.md +35 -7
- package/skills/insight-binary/SKILL.md +1 -1
- package/skills/insight-capture/SKILL.md +1 -1
- package/skills/insight-die/SKILL.md +1 -1
- package/skills/insight-dnspy/SKILL.md +1 -1
- package/skills/insight-dotnet-dump/SKILL.md +1 -1
- package/skills/insight-dotnet-stack/SKILL.md +1 -1
- package/skills/insight-dotnet-trace/SKILL.md +1 -1
- package/skills/insight-frida/SKILL.md +1 -1
- package/skills/insight-ghidra/SKILL.md +14 -1
- package/skills/insight-ghidra/references/persistent-client.mjs +125 -0
- package/skills/insight-ghidra/references/persistent.md +50 -0
- package/skills/insight-ghidra/references/query-semantics.md +12 -0
- package/skills/insight-ghidra/references/recovery.md +36 -0
- package/skills/insight-ghidra/references/research.md +87 -0
- package/skills/insight-ghidra/references/setup.md +1 -1
- package/skills/insight-ghidra/references/sparse-navigation.md +173 -0
- package/skills/insight-ghidra/references/storage.md +35 -7
- package/skills/insight-ilspy/SKILL.md +1 -1
- package/skills/insight-interop/SKILL.md +1 -1
- package/skills/insight-lldb/SKILL.md +1 -1
- package/skills/insight-llvm/SKILL.md +1 -1
- package/skills/insight-managed-heap/SKILL.md +1 -1
- package/skills/insight-managed-metadata/SKILL.md +1 -1
- package/skills/insight-mitmproxy/SKILL.md +1 -1
- package/skills/insight-overview/SKILL.md +1 -1
- package/skills/insight-procmon/SKILL.md +1 -1
- package/skills/insight-re/SKILL.md +4 -1
- package/skills/insight-re/references/persistent-client.mjs +125 -0
- package/skills/insight-re/references/persistent.md +50 -0
- package/skills/insight-re/references/recovery.md +36 -0
- package/skills/insight-re/references/storage.md +35 -7
- package/skills/insight-sevenzip/SKILL.md +1 -1
- package/skills/insight-strace/SKILL.md +1 -1
- package/skills/insight-tshark/SKILL.md +1 -1
- package/skills/insight-windbg/SKILL.md +1 -1
|
@@ -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
|
-
|
|
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 =
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
fail('partial', String(single.error), single);
|
|
40
|
+
catch (error) {
|
|
41
|
+
await guard();
|
|
42
|
+
throw error;
|
|
65
43
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
for (const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
}>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, writeFile, readFile, open } from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { object, fail } from './model.js';
|
|
6
|
+
export const inlineSourceHash = (code) => createHash('sha256').update(code, 'utf8').digest('hex');
|
|
7
|
+
/** Native facts come from the fixed helper's receipt, never from caller console output. */
|
|
8
|
+
export function inlineReceipt(raw, expected) {
|
|
9
|
+
const r = object(raw), execution = object(r.execution), build = object(r.compilerOutput);
|
|
10
|
+
if (r.protocol !== 'insight-inline-v1' || r.attemptId !== expected.attemptId || r.requestId !== expected.requestId || r.sourceSha256 !== expected.sourceSha256 ||
|
|
11
|
+
typeof r.success !== 'boolean' || !['prepare', 'compile', 'load', 'initialize', 'construct', 'execute'].includes(String(execution.stage)) ||
|
|
12
|
+
!['not_started', 'may_have_started'].includes(String(execution.userCode)) || ![false, null].includes(execution.entrypointInvoked) ||
|
|
13
|
+
typeof build.text !== 'string' || Buffer.byteLength(build.text) > 32772 || typeof build.truncated !== 'boolean' ||
|
|
14
|
+
typeof r.console_output !== 'string' || Buffer.byteLength(r.console_output) > 32772 || typeof r.outputTruncated !== 'boolean')
|
|
15
|
+
fail('partial', 'Native inline-script receipt is absent, malformed or belongs to another attempt.');
|
|
16
|
+
if (r.success === false) {
|
|
17
|
+
const d = object(r.diagnostic);
|
|
18
|
+
if (typeof d.kind !== 'string' || typeof d.message !== 'string' || d.message.length > 2048)
|
|
19
|
+
fail('partial', 'Native inline-script failure omitted its diagnostic.');
|
|
20
|
+
}
|
|
21
|
+
if (r.success === true && (execution.userCode !== 'may_have_started' || execution.stage !== 'execute'))
|
|
22
|
+
fail('partial', 'Native inline-script completion contradicts its execution facts.');
|
|
23
|
+
const safe = r.success === false && execution.userCode === 'not_started' && execution.entrypointInvoked === false && ['prepare', 'compile', 'load'].includes(String(execution.stage));
|
|
24
|
+
if (execution.userCode === 'not_started' && !safe)
|
|
25
|
+
fail('partial', 'Native inline-script non-execution facts contradict their stage.');
|
|
26
|
+
return { response: r, safe };
|
|
27
|
+
}
|
|
28
|
+
export async function runInline(http, program, root, parameters, requestId) {
|
|
29
|
+
if (typeof parameters.code !== 'string' || Buffer.byteLength(parameters.code) > 1048576 || parameters.code.includes('\0'))
|
|
30
|
+
fail('invalid', 'Inline Java source must contain at most one MiB of UTF-8 text without NUL.');
|
|
31
|
+
const attemptId = randomUUID(), sourceSha256 = inlineSourceHash(parameters.code), directory = join(root, 'inline-attempts', attemptId);
|
|
32
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
33
|
+
const request = join(directory, 'request.json');
|
|
34
|
+
await writeFile(request, JSON.stringify({ attemptId, requestId, sourceSha256, code: parameters.code, args: parameters.args ?? '' }), { mode: 0o600, flag: 'wx' });
|
|
35
|
+
const name = 'InsightInlineScript_' + randomUUID().replaceAll('-', ''), source = await readFile(new URL('../../references/native/InsightInlineScript.java', import.meta.url), 'utf8');
|
|
36
|
+
// Only this fixed shim enters the upstream provider. User source is compiled separately.
|
|
37
|
+
const ack = object(await http.call('/run_script_inline', { program, code: source.replace('public class InsightInlineScript ', 'public class ' + name + ' '), args: Buffer.from(request, 'utf8').toString('base64') }));
|
|
38
|
+
if (ack.success !== true || typeof ack.console_output !== 'string' || !ack.console_output.includes('INSIGHT_INLINE_COMPLETE'))
|
|
39
|
+
fail('partial', 'Native inline-script executor did not acknowledge its completed attempt.');
|
|
40
|
+
const handle = await open(join(directory, 'receipt.json'), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
41
|
+
let raw;
|
|
42
|
+
try {
|
|
43
|
+
const maximum = 1048576, stat = await handle.stat();
|
|
44
|
+
if (!stat.isFile() || stat.size > maximum)
|
|
45
|
+
fail('partial', 'Native inline-script receipt exceeds its bound.');
|
|
46
|
+
const bytes = Buffer.alloc(maximum + 1);
|
|
47
|
+
let length = 0;
|
|
48
|
+
while (length < bytes.length) {
|
|
49
|
+
const next = await handle.read(bytes, length, bytes.length - length, null);
|
|
50
|
+
if (!next.bytesRead)
|
|
51
|
+
break;
|
|
52
|
+
length += next.bytesRead;
|
|
53
|
+
}
|
|
54
|
+
if (length > maximum)
|
|
55
|
+
fail('partial', 'Native inline-script receipt exceeds its bound.');
|
|
56
|
+
raw = JSON.parse(bytes.subarray(0, length).toString('utf8'));
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
await handle.close();
|
|
60
|
+
}
|
|
61
|
+
return inlineReceipt(raw, { attemptId, requestId, sourceSha256 });
|
|
62
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Internal diagnostics only: fixed operation names, never argv, payloads, paths or errors. */
|
|
2
|
+
export declare function timed<T>(phase: string, operation: string, action: () => Promise<T>): Promise<T>;
|
|
3
|
+
export declare function traceCommand<T>(root: string, command: string, action: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { mkdir, appendFile, stat, rename } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { performance } from 'node:perf_hooks';
|
|
6
|
+
const current = new AsyncLocalStorage(), writes = new Map();
|
|
7
|
+
/** Internal diagnostics only: fixed operation names, never argv, payloads, paths or errors. */
|
|
8
|
+
export async function timed(phase, operation, action) {
|
|
9
|
+
const trace = current.getStore();
|
|
10
|
+
if (!trace)
|
|
11
|
+
return action();
|
|
12
|
+
const span = randomUUID(), started = performance.now();
|
|
13
|
+
let outcome = 'success';
|
|
14
|
+
try {
|
|
15
|
+
return await current.run({ ...trace, parent: span }, action);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
outcome = 'failure';
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
if (trace.rows.length < 1024)
|
|
23
|
+
trace.rows.push({ trace: trace.id, span, parent: trace.parent ?? null, command: trace.command, phase, operation, startedMs: started - trace.started, durationMs: performance.now() - started, outcome });
|
|
24
|
+
else
|
|
25
|
+
trace.overflow.count++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function traceCommand(root, command, action) {
|
|
29
|
+
const trace = { id: randomUUID(), command, started: performance.now(), rows: [], overflow: { count: 0 } }, at = new Date().toISOString();
|
|
30
|
+
try {
|
|
31
|
+
return await current.run(trace, () => timed('facade.command', command, action));
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
const directory = join(root, 'diagnostics'), path = join(directory, 'timings.jsonl'), previous = writes.get(path) ?? Promise.resolve();
|
|
35
|
+
const pending = previous.catch(() => { }).then(async () => {
|
|
36
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
37
|
+
if (await stat(path).then(s => s.size, () => 0) > 8 * 1024 ** 2)
|
|
38
|
+
await rename(path, path + '.previous');
|
|
39
|
+
await appendFile(path, trace.rows.map(row => JSON.stringify({ schema: 1, at, ...row, droppedSpans: trace.overflow.count })).join('\n') + '\n', { mode: 0o600 });
|
|
40
|
+
});
|
|
41
|
+
writes.set(path, pending);
|
|
42
|
+
// Telemetry loss cannot change an acknowledged program operation into a failed write.
|
|
43
|
+
try {
|
|
44
|
+
await pending;
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
finally {
|
|
48
|
+
if (writes.get(path) === pending)
|
|
49
|
+
writes.delete(path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/src/lifecycle.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { command, Permission } from '@eyeauras/cli-factory';
|
|
1
|
+
import { command, Permission, CliError } from '@eyeauras/cli-factory';
|
|
2
2
|
import { scope } from './research.js';
|
|
3
3
|
import { id, fail, object, digest } from './model.js';
|
|
4
4
|
import { selectedOptions } from './arguments.js';
|
|
@@ -24,13 +24,23 @@ export class Lifecycle {
|
|
|
24
24
|
async change(selected, operation, callerId) {
|
|
25
25
|
const reserved = this.workspace.reserveRequest(selected, 'ghidra.' + operation, {}, callerId);
|
|
26
26
|
return this.research.ordered(selected, async () => {
|
|
27
|
-
|
|
27
|
+
let request = this.workspace.request(reserved.request.id), backend;
|
|
28
|
+
if (reserved.replay && ['running', 'unknown'].includes(request.state)) {
|
|
29
|
+
backend = await this.backend();
|
|
30
|
+
await this.research.reconcileRequests(selected, backend);
|
|
31
|
+
request = this.workspace.request(request.id);
|
|
32
|
+
}
|
|
28
33
|
if (reserved.replay && request.state !== 'accepted') {
|
|
29
34
|
if (request.state === 'applied')
|
|
30
35
|
return request.result;
|
|
31
|
-
|
|
36
|
+
throw new CliError('This lifecycle outcome remains unresolved; inspect its retained worker before retrieving it again.', {
|
|
37
|
+
code: 'coordinator.unknown', result: { ...selected, state: request.state, requestId: request.callerId ?? request.id },
|
|
38
|
+
next: [['sessions', 'status', selected.session, '--target', selected.target],
|
|
39
|
+
...(operation === 'close' ? [['tools', 'processes', '--tool', 'ghidra', '--session', selected.session, '--target', selected.target]] : [['ghidra', 'open', '--session', selected.session, '--target', selected.target]])]
|
|
40
|
+
});
|
|
32
41
|
}
|
|
33
|
-
|
|
42
|
+
backend ??= await this.backend();
|
|
43
|
+
const unresolved = this.workspace.unresolved(selected);
|
|
34
44
|
const closingExternal = operation === 'close' && unresolved.length > 0 && unresolved.every(intent => ['debugger', 'ui'].includes(String(object(intent.execution ?? {}).kind)));
|
|
35
45
|
const alreadyEnded = closingExternal && backend.closed ? await backend.closed(selected) : false;
|
|
36
46
|
if (!closingExternal)
|
|
@@ -11,7 +11,7 @@ export interface NativeInputs {
|
|
|
11
11
|
extensionRoots?: string[];
|
|
12
12
|
serverClasspath?: string;
|
|
13
13
|
}
|
|
14
|
-
export declare const pinnedMcpJar = "
|
|
14
|
+
export declare const pinnedMcpJar = "f4a9272e911466ed774c8cf73bc1adf1daef6009d895fda57aa0b28522fcdf32";
|
|
15
15
|
export declare const packagedMcpJar: string;
|
|
16
16
|
/** Ghidra has its own cache/settings overrides in addition to Java's home/tmp properties. */
|
|
17
17
|
export declare const nativeJvmDirectories: (root: string) => string[];
|