@eyeauras/insight-cli 0.0.0-stage → 0.1.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 -0
- package/LICENSE +21 -0
- package/README.md +54 -2
- package/SKILL.md +55 -0
- package/dist/src/administration-results.d.ts +1 -0
- package/dist/src/administration-results.js +84 -0
- package/dist/src/administration.d.ts +14 -0
- package/dist/src/administration.js +120 -0
- package/dist/src/archive-contract.d.ts +6 -0
- package/dist/src/archive-contract.js +108 -0
- package/dist/src/archive.d.ts +12 -0
- package/dist/src/archive.js +116 -0
- package/dist/src/arguments.d.ts +14 -0
- package/dist/src/arguments.js +62 -0
- package/dist/src/artifacts.d.ts +17 -0
- package/dist/src/artifacts.js +115 -0
- package/dist/src/bin.d.ts +2 -0
- package/dist/src/bin.js +4 -0
- package/dist/src/cli.d.ts +10 -0
- package/dist/src/cli.js +387 -0
- package/dist/src/comment-effects.d.ts +3 -0
- package/dist/src/comment-effects.js +55 -0
- package/dist/src/contexts.d.ts +115 -0
- package/dist/src/contexts.js +1068 -0
- package/dist/src/coordinator.d.ts +41 -0
- package/dist/src/coordinator.js +160 -0
- package/dist/src/cross-results.d.ts +14 -0
- package/dist/src/cross-results.js +71 -0
- package/dist/src/cross.d.ts +3 -0
- package/dist/src/cross.js +43 -0
- package/dist/src/data-effects.d.ts +3 -0
- package/dist/src/data-effects.js +103 -0
- package/dist/src/debugger-results.d.ts +4 -0
- package/dist/src/debugger-results.js +151 -0
- package/dist/src/debugger-supervisor.d.ts +1 -0
- package/dist/src/debugger-supervisor.js +136 -0
- package/dist/src/debugger.d.ts +21 -0
- package/dist/src/debugger.js +267 -0
- package/dist/src/documentation-effects.d.ts +5 -0
- package/dist/src/documentation-effects.js +56 -0
- package/dist/src/extensions.d.ts +64 -0
- package/dist/src/extensions.js +289 -0
- package/dist/src/function-effects.d.ts +4 -0
- package/dist/src/function-effects.js +112 -0
- package/dist/src/ghidra.d.ts +50 -0
- package/dist/src/ghidra.js +468 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/initial-model.d.ts +6 -0
- package/dist/src/initial-model.js +85 -0
- package/dist/src/json.d.ts +2 -0
- package/dist/src/json.js +8 -0
- package/dist/src/lifecycle.d.ts +49 -0
- package/dist/src/lifecycle.js +148 -0
- package/dist/src/memory-inputs.d.ts +8 -0
- package/dist/src/memory-inputs.js +56 -0
- package/dist/src/merge.d.ts +29 -0
- package/dist/src/merge.js +66 -0
- package/dist/src/model.d.ts +74 -0
- package/dist/src/model.js +67 -0
- package/dist/src/mutation-effects.d.ts +4 -0
- package/dist/src/mutation-effects.js +321 -0
- package/dist/src/mutation-inputs.d.ts +2 -0
- package/dist/src/mutation-inputs.js +71 -0
- package/dist/src/mutations.d.ts +4 -0
- package/dist/src/mutations.js +53 -0
- package/dist/src/native-inputs.d.ts +28 -0
- package/dist/src/native-inputs.js +171 -0
- package/dist/src/native-queries.d.ts +2 -0
- package/dist/src/native-queries.js +29 -0
- package/dist/src/projects.d.ts +31 -0
- package/dist/src/projects.js +301 -0
- package/dist/src/prototype-cli.d.ts +8 -0
- package/dist/src/prototype-cli.js +155 -0
- package/dist/src/queries.d.ts +5 -0
- package/dist/src/queries.js +84 -0
- package/dist/src/query-policy.d.ts +6 -0
- package/dist/src/query-policy.js +72 -0
- package/dist/src/query-results.d.ts +2 -0
- package/dist/src/query-results.js +224 -0
- package/dist/src/research.d.ts +184 -0
- package/dist/src/research.js +808 -0
- package/dist/src/saved-materials.d.ts +12 -0
- package/dist/src/saved-materials.js +196 -0
- package/dist/src/scripts.d.ts +19 -0
- package/dist/src/scripts.js +79 -0
- package/dist/src/sdk-contracts.d.ts +13 -0
- package/dist/src/sdk-contracts.js +37 -0
- package/dist/src/sdk-files.d.ts +29 -0
- package/dist/src/sdk-files.js +249 -0
- package/dist/src/sdk.d.ts +4 -0
- package/dist/src/sdk.js +125 -0
- package/dist/src/server.d.ts +44 -0
- package/dist/src/server.js +455 -0
- package/dist/src/store.d.ts +33 -0
- package/dist/src/store.js +178 -0
- package/dist/src/sync-policy.d.ts +22 -0
- package/dist/src/sync-policy.js +123 -0
- package/dist/src/transport.d.ts +23 -0
- package/dist/src/transport.js +135 -0
- package/dist/src/ui-results.d.ts +2 -0
- package/dist/src/ui-results.js +80 -0
- package/dist/src/ui.d.ts +17 -0
- package/dist/src/ui.js +88 -0
- package/dist/src/utility.d.ts +14 -0
- package/dist/src/utility.js +43 -0
- package/dist/src/version-tracking.d.ts +57 -0
- package/dist/src/version-tracking.js +200 -0
- package/dist/src/workspace.d.ts +199 -0
- package/dist/src/workspace.js +564 -0
- package/package.json +45 -4
- package/references/InsightRepositoryProbe.java +55 -0
- package/references/administration.md +33 -0
- package/references/archive.md +32 -0
- package/references/artifacts.md +47 -0
- package/references/debugger.md +40 -0
- package/references/extensions-and-sync.md +124 -0
- package/references/mutations.json +1424 -0
- package/references/native/InsightApplyDocumentation.java +48 -0
- package/references/native/InsightArchivePayloads.java +36 -0
- package/references/native/InsightBlockTransfer.java +57 -0
- package/references/native/InsightEmulation.java +93 -0
- package/references/native/InsightExtensions.java +36 -0
- package/references/native/InsightFunctionDatabases.java +98 -0
- package/references/native/InsightGuiMain.java +32 -0
- package/references/native/InsightHeadlessMain.java +10 -0
- package/references/native/InsightInitialImport.java +42 -0
- package/references/native/InsightInitialJournal.java +48 -0
- package/references/native/InsightInitialPublication.java +142 -0
- package/references/native/InsightKeytool.java +16 -0
- package/references/native/InsightNativeReceipt.java +31 -0
- package/references/native/InsightPlugins.java +34 -0
- package/references/native/InsightProjectFiles.java +92 -0
- package/references/native/InsightPublicationRecovery.java +63 -0
- package/references/native/InsightPythonWheels.java +31 -0
- package/references/native/InsightResearchQueries.java +188 -0
- package/references/native/InsightSdkFiles.java +142 -0
- package/references/native/InsightSdkScript.java +31 -0
- package/references/native/InsightSdkTools.java +338 -0
- package/references/native/InsightSdkTypes.java +132 -0
- package/references/native/InsightServerAdministration.java +82 -0
- package/references/native/InsightServerInit.java +62 -0
- package/references/native/InsightServerMain.java +43 -0
- package/references/native/InsightServerProbe.java +29 -0
- package/references/native/InsightSnapshotFields.java +68 -0
- package/references/native/InsightSnapshotInspect.java +189 -0
- package/references/native/InsightSnapshotPublisher.java +123 -0
- package/references/native/InsightStartDebugger.java +28 -0
- package/references/native/InsightStopDebugger.java +20 -0
- package/references/native/InsightTraceInspect.java +24 -0
- package/references/native/InsightTraceState.java +130 -0
- package/references/native/InsightTraceTools.java +114 -0
- package/references/native/InsightVersionTracking.java +91 -0
- package/references/native-workflows.md +112 -0
- package/references/queries.json +1761 -0
- package/references/query-semantics.md +96 -0
- package/references/recovery.md +53 -0
- package/references/research.md +101 -0
- package/references/setup.md +47 -0
- package/runtime/GhidraMCP-7.0.0.jar +0 -0
- package/runtime/GhidraMCP-LICENSE +202 -0
- package/runtime/GhidraMCP-NOTICE +10 -0
- package/runtime/provenance.json +11 -0
- package/skills/insight-re/SKILL.md +108 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { promisify } from 'node:util';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { spawn, execFile } from 'node:child_process';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { mkdir, open, readFile, readdir, cp, realpath, stat } from 'node:fs/promises';
|
|
6
|
+
import { join, delimiter, resolve, isAbsolute, sep } from 'node:path';
|
|
7
|
+
import { createServer } from 'node:net';
|
|
8
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
9
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
10
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
11
|
+
import { assertIdentity, fail, key, object, text, version } from './model.js';
|
|
12
|
+
import { HttpBridge, decodeMcp } from './transport.js';
|
|
13
|
+
async function durable(path, value) {
|
|
14
|
+
const file = await open(path, 'wx', 0o600);
|
|
15
|
+
try {
|
|
16
|
+
await file.writeFile(JSON.stringify(value));
|
|
17
|
+
await file.sync();
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await file.close();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const alive = (pid) => { try {
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
if (e.code === 'ESRCH')
|
|
29
|
+
return false;
|
|
30
|
+
throw e;
|
|
31
|
+
} };
|
|
32
|
+
async function freePort() {
|
|
33
|
+
const server = createServer();
|
|
34
|
+
await new Promise((ok, bad) => { server.once('error', bad); server.listen(0, '127.0.0.1', ok); });
|
|
35
|
+
const address = server.address();
|
|
36
|
+
if (!address || typeof address === 'string')
|
|
37
|
+
fail('launch', 'Could not allocate a worker endpoint.');
|
|
38
|
+
await new Promise(ok => server.close(() => ok()));
|
|
39
|
+
return address.port;
|
|
40
|
+
}
|
|
41
|
+
export class GhidraRuntime {
|
|
42
|
+
directory;
|
|
43
|
+
context;
|
|
44
|
+
processes;
|
|
45
|
+
constructor(directory, context, processes = new Map()) {
|
|
46
|
+
this.directory = directory;
|
|
47
|
+
this.context = context;
|
|
48
|
+
this.processes = processes;
|
|
49
|
+
}
|
|
50
|
+
fork(context) { return new GhidraRuntime(this.directory, context, this.processes); }
|
|
51
|
+
async launch(t, label, role) {
|
|
52
|
+
assertIdentity(t, { ...t, profile: this.context.profile.name, server: String(this.context.profile.values.server) });
|
|
53
|
+
const values = this.context.profile.values;
|
|
54
|
+
const java = text(values.java), home = text(values.ghidraHome), jar = text(values.mcpJar), expected = text(values.mcpJarSha256).toLowerCase();
|
|
55
|
+
if (![java, home, jar].every(isAbsolute))
|
|
56
|
+
fail('configuration', 'Java, Ghidra home and MCP jar must be absolute paths.');
|
|
57
|
+
if (createHash('sha256').update(await readFile(jar)).digest('hex') !== expected)
|
|
58
|
+
fail('binding', 'GhidraMCP jar SHA256 does not match the configured artifact.');
|
|
59
|
+
const root = join(this.directory, 'processes', label);
|
|
60
|
+
if (resolve(root).split(sep).some(p => p.startsWith('.')))
|
|
61
|
+
fail('configuration', 'Ghidra project AppData path contains a hidden component; use a supported OS AppData root.');
|
|
62
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
63
|
+
const receipt = join(root, 'process.json');
|
|
64
|
+
try {
|
|
65
|
+
await readFile(receipt);
|
|
66
|
+
fail('blocked', 'Process stage already exists; recover it instead of creating a new writer.');
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
if (e.code !== 'ENOENT')
|
|
70
|
+
throw e;
|
|
71
|
+
}
|
|
72
|
+
const jars = [jar];
|
|
73
|
+
const collect = async (folder) => { for (const item of await readdir(folder, { withFileTypes: true })) {
|
|
74
|
+
const path = join(folder, item.name);
|
|
75
|
+
if (item.isDirectory())
|
|
76
|
+
await collect(path);
|
|
77
|
+
else if (item.isFile() && path.endsWith('.jar'))
|
|
78
|
+
jars.push(path);
|
|
79
|
+
} };
|
|
80
|
+
await collect(join(home, 'Ghidra'));
|
|
81
|
+
const port = await freePort();
|
|
82
|
+
const url = new URL(t.server);
|
|
83
|
+
const user = text(values[role === 'reader' ? 'readerUser' : 'publisherUser']);
|
|
84
|
+
if (values.readerUser === values.publisherUser)
|
|
85
|
+
fail('authorization', 'Reader and publisher must use distinct Ghidra Server accounts.');
|
|
86
|
+
const password = await this.context.secrets.require(role === 'reader' ? 'readerPassword' : 'publisherPassword');
|
|
87
|
+
const bridgeSecret = role === 'publisher' ? 'bridge-' + label : undefined;
|
|
88
|
+
const bridgeToken = bridgeSecret ? randomUUID() + randomUUID() : undefined;
|
|
89
|
+
if (bridgeSecret && bridgeToken)
|
|
90
|
+
await this.context.secrets.set(bridgeSecret, bridgeToken);
|
|
91
|
+
const log = await open(join(root, 'process.log'), 'a', 0o600);
|
|
92
|
+
const child = spawn(java, ['-Xmx768m', '-Djava.awt.headless=true', '-Dghidra.home=' + home, '-Dapplication.name=GhidraMCP', '-Duser.home=' + root, '-Djava.io.tmpdir=' + root, '-cp', jars.join(delimiter), 'com.xebyte.headless.GhidraMCPHeadlessServer', '--bind', '127.0.0.1', '--port', String(port)], {
|
|
93
|
+
cwd: root, detached: true, stdio: ['ignore', log.fd, log.fd], windowsHide: true,
|
|
94
|
+
env: { PATH: this.context.environment.PATH ?? process.env.PATH, HOME: root, TMPDIR: root, XDG_RUNTIME_DIR: this.directory,
|
|
95
|
+
GHIDRA_SERVER_HOST: url.hostname, GHIDRA_SERVER_PORT: url.port || '13100', GHIDRA_SERVER_USER: user, GHIDRA_SERVER_PASSWORD: password,
|
|
96
|
+
GHIDRA_MCP_SHARED_PROJECT_DIR: root, GHIDRA_MCP_FILE_ROOT: this.directory, GHIDRA_MCP_ALLOW_SCRIPTS: '0', GHIDRA_MCP_PROJECT_FOLDER: t.programPath, ...(bridgeToken ? { GHIDRA_MCP_AUTH_TOKEN: bridgeToken } : {}) },
|
|
97
|
+
});
|
|
98
|
+
child.on('error', () => { });
|
|
99
|
+
child.unref();
|
|
100
|
+
await log.close();
|
|
101
|
+
if (!child.pid)
|
|
102
|
+
fail('launch', 'Could not start the owned Ghidra process.');
|
|
103
|
+
const binding = { pid: child.pid, endpoint: `http://127.0.0.1:${port}`, projectDirectory: root, identity: t, version: 1, role, ...(bridgeSecret ? { bridgeSecret } : {}) };
|
|
104
|
+
await durable(receipt, binding);
|
|
105
|
+
const http = new HttpBridge(binding.endpoint, this.context.fetch, this.context.signal, bridgeToken);
|
|
106
|
+
this.processes.set(root, { binding, http, child });
|
|
107
|
+
try {
|
|
108
|
+
const deadline = Date.now() + 60000;
|
|
109
|
+
let ready = false;
|
|
110
|
+
while (Date.now() < deadline && alive(child.pid) && !this.context.signal.aborted) {
|
|
111
|
+
try {
|
|
112
|
+
const info = await http.instance();
|
|
113
|
+
if (info.pid !== child.pid || info.tcp_port !== port)
|
|
114
|
+
fail('binding', 'Endpoint belongs to another worker process.');
|
|
115
|
+
ready = true;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
await delay(100);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!ready)
|
|
123
|
+
fail('launch', 'Owned Ghidra process did not become ready; its stage is retained.');
|
|
124
|
+
await http.call('/server/connect');
|
|
125
|
+
const project = object(await http.call('/open_project', { path: t.server + '/' + t.repository }));
|
|
126
|
+
if (project.shared !== true)
|
|
127
|
+
fail('binding', 'Ghidra project is not shared.');
|
|
128
|
+
const projectInfo = object(await http.call('/get_project_info'));
|
|
129
|
+
if (projectInfo.project_server_bound !== true || projectInfo.server_repo !== t.repository)
|
|
130
|
+
fail('binding', 'Ghidra project is not verified as server-bound.');
|
|
131
|
+
if (project.repository !== t.repository)
|
|
132
|
+
fail('binding', 'Opened repository differs from the requested target.');
|
|
133
|
+
const opening = await this.probe(t, role);
|
|
134
|
+
if (role === 'reader') {
|
|
135
|
+
const opened = object(await http.call('/open_program', { path: t.programPath }));
|
|
136
|
+
if (opened.path !== t.programPath || opened.read_only !== true)
|
|
137
|
+
fail('authorization', 'Reader program did not open read-only.');
|
|
138
|
+
const afterOpen = await this.probe(t, role);
|
|
139
|
+
if (opening.version !== afterOpen.version)
|
|
140
|
+
fail('binding', 'Published version changed while opening the reader snapshot.');
|
|
141
|
+
binding.version = version(opening.version);
|
|
142
|
+
binding.openingVersion = binding.version;
|
|
143
|
+
}
|
|
144
|
+
return binding;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (role === 'reader')
|
|
148
|
+
await this.abortReader(t, binding);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async open(t, batchId) { return this.launch(t, batchId, 'publisher'); }
|
|
153
|
+
async receipt(t, b) {
|
|
154
|
+
assertIdentity(b.identity, t);
|
|
155
|
+
const root = resolve(b.projectDirectory);
|
|
156
|
+
if (!root.startsWith(resolve(this.directory, 'processes') + sep))
|
|
157
|
+
fail('binding', 'Process stage is outside the selected profile.');
|
|
158
|
+
const saved = object(JSON.parse(await readFile(join(root, 'process.json'), 'utf8')));
|
|
159
|
+
if (saved.pid !== b.pid || saved.endpoint !== b.endpoint || saved.role !== b.role || saved.bridgeSecret !== b.bridgeSecret || await realpath(root) !== root)
|
|
160
|
+
fail('binding', 'Owned process receipt does not match the requested binding.');
|
|
161
|
+
assertIdentity(saved.identity, t);
|
|
162
|
+
return root;
|
|
163
|
+
}
|
|
164
|
+
async record(t, b) {
|
|
165
|
+
const root = await this.receipt(t, b);
|
|
166
|
+
if (b.role === 'publisher' && !b.bridgeSecret)
|
|
167
|
+
fail('authorization', 'Publisher endpoint lacks a scoped authentication secret.');
|
|
168
|
+
const bridgeToken = b.bridgeSecret ? await this.context.secrets.require(b.bridgeSecret) : undefined;
|
|
169
|
+
const rec = { ...(this.processes.get(root) ?? { binding: b }), http: new HttpBridge(b.endpoint, this.context.fetch, this.context.signal, bridgeToken) };
|
|
170
|
+
const info = await rec.http.instance();
|
|
171
|
+
if (info.pid !== b.pid || info.tcp_port !== Number(new URL(b.endpoint).port))
|
|
172
|
+
fail('binding', 'Actual worker PID or endpoint changed.');
|
|
173
|
+
const projectPath = text(info.project_path, 4096);
|
|
174
|
+
if (!projectPath.startsWith(root + sep))
|
|
175
|
+
fail('binding', 'Actual worker project directory differs from its owned stage.');
|
|
176
|
+
this.processes.set(root, rec);
|
|
177
|
+
return rec;
|
|
178
|
+
}
|
|
179
|
+
async boundIdentity(t, rec) {
|
|
180
|
+
const project = object(await rec.http.call('/get_project_info'));
|
|
181
|
+
const url = new URL(t.server);
|
|
182
|
+
if (project.project_server_bound !== true || project.server_repo !== t.repository || project.server_info !== url.hostname + ':' + (url.port || '13100') || project.server_connected !== true)
|
|
183
|
+
fail('binding', 'Actual server/repository binding differs from the registered target.');
|
|
184
|
+
const p = { program: t.programPath };
|
|
185
|
+
const metadata = object(await rec.http.call('/get_metadata', p));
|
|
186
|
+
const options = object(await rec.http.call('/get_program_options', { ...p, group: 'Program Information' }));
|
|
187
|
+
if (!Array.isArray(options.options))
|
|
188
|
+
fail('malformed', 'Program options are missing.');
|
|
189
|
+
const sha = options.options.filter(x => object(x).name === 'Executable SHA256');
|
|
190
|
+
if (sha.length !== 1)
|
|
191
|
+
fail('binding', 'Executable SHA256 is missing or ambiguous.');
|
|
192
|
+
const actual = { ...t, binarySha256: text(object(sha[0]).value).toLowerCase(), language: text(metadata.language), compilerSpec: text(metadata.compiler), imageBase: text(metadata.base_address).toLowerCase() };
|
|
193
|
+
const info = await rec.http.instance();
|
|
194
|
+
if (!Array.isArray(info.programs) || !info.programs.some(x => object(x).path === t.programPath && object(x).open === true))
|
|
195
|
+
fail('binding', 'The explicit program path is not open in the selected worker.');
|
|
196
|
+
assertIdentity(actual, t);
|
|
197
|
+
return actual;
|
|
198
|
+
}
|
|
199
|
+
async latest(t, rec) {
|
|
200
|
+
const history = object(await rec.http.call('/server/version_history', { path: t.programPath }));
|
|
201
|
+
if (!Array.isArray(history.versions) || history.versions.length !== history.count || !history.versions.length)
|
|
202
|
+
fail('malformed', 'Server history is incomplete.');
|
|
203
|
+
return Math.max(...history.versions.map(x => version(object(x).version)));
|
|
204
|
+
}
|
|
205
|
+
async localVersion(t, rec) {
|
|
206
|
+
const r = object(await rec.http.call('/list_project_files', { folder: t.programPath.slice(0, t.programPath.lastIndexOf('/')) || '/' }));
|
|
207
|
+
if (!Array.isArray(r.files))
|
|
208
|
+
fail('malformed', 'Project file inventory is missing.');
|
|
209
|
+
const rows = r.files.filter(x => object(x).path === t.programPath);
|
|
210
|
+
if (rows.length !== 1)
|
|
211
|
+
fail('binding', 'Program path is absent or ambiguous in the project.');
|
|
212
|
+
const row = object(rows[0]);
|
|
213
|
+
if (row.is_versioned !== true)
|
|
214
|
+
fail('binding', 'Program is not versioned.');
|
|
215
|
+
return version(row.checkout_version ?? row.version);
|
|
216
|
+
}
|
|
217
|
+
async checkout(t, b) {
|
|
218
|
+
const rec = await this.record(t, b);
|
|
219
|
+
const result = object(await rec.http.call('/server/version_control/checkout', { path: t.programPath, exclusive: true }));
|
|
220
|
+
if (result.success !== true || result.status !== 'checked_out' || result.exclusive !== true || result.is_checked_out !== true || result.path !== t.programPath || result.repository !== t.repository)
|
|
221
|
+
fail('checkout', 'Exclusive checkout was not fully verified.');
|
|
222
|
+
await rec.http.call('/open_program', { path: t.programPath });
|
|
223
|
+
const identity = await this.boundIdentity(t, rec);
|
|
224
|
+
b.version = await this.localVersion(t, rec);
|
|
225
|
+
if (b.version !== await this.latest(t, rec))
|
|
226
|
+
fail('binding', 'Checkout is not based on the latest published version.');
|
|
227
|
+
return { identity, version: b.version, values: {} };
|
|
228
|
+
}
|
|
229
|
+
async read(t, b, findings) {
|
|
230
|
+
const rec = await this.record(t, b);
|
|
231
|
+
const identity = await this.boundIdentity(t, rec);
|
|
232
|
+
const values = {};
|
|
233
|
+
for (const f of findings) {
|
|
234
|
+
const observed = object(await rec.http.call('/get_comment', { address: f.address, program: t.programPath }));
|
|
235
|
+
if (typeof observed.address !== 'string' || observed.address.toLowerCase() !== f.address || !Object.hasOwn(observed, f.kind))
|
|
236
|
+
fail('partial', 'Comment readback omitted or changed its selector.');
|
|
237
|
+
const value = observed[f.kind];
|
|
238
|
+
if (value !== null && typeof value !== 'string')
|
|
239
|
+
fail('malformed', 'Comment value is malformed.');
|
|
240
|
+
values[key(f)] = value;
|
|
241
|
+
}
|
|
242
|
+
const localVersion = await this.localVersion(t, rec);
|
|
243
|
+
if (localVersion !== b.version)
|
|
244
|
+
fail('binding', 'Worker baseline changed.');
|
|
245
|
+
return { identity, version: localVersion, values };
|
|
246
|
+
}
|
|
247
|
+
async apply(t, b, f) {
|
|
248
|
+
if (b.role !== 'publisher')
|
|
249
|
+
fail('authorization', 'Only the coordinator publisher can write.');
|
|
250
|
+
const rec = await this.record(t, b);
|
|
251
|
+
await this.boundIdentity(t, rec);
|
|
252
|
+
const r = object(await rec.http.call('/set_comment', { program: t.programPath, address: f.address, type: f.kind, comment: f.after }));
|
|
253
|
+
if (r.status !== 'success')
|
|
254
|
+
fail('partial', 'Ghidra did not acknowledge this comment operation.');
|
|
255
|
+
}
|
|
256
|
+
async save(t, b) { const rec = await this.record(t, b); const r = object(await rec.http.call('/save_program', { program: t.programPath })); if (r.success !== true)
|
|
257
|
+
fail('save', 'Ghidra did not acknowledge saving the local stage.'); }
|
|
258
|
+
async checkIn(t, b, batchId) {
|
|
259
|
+
const rec = await this.record(t, b);
|
|
260
|
+
await this.boundIdentity(t, rec);
|
|
261
|
+
const r = object(await rec.http.call('/checkin_program', { path: t.programPath, comment: 'insight batch ' + batchId, keep_checked_out: false }));
|
|
262
|
+
if (r.success !== true || r.status !== 'checked_in' || r.version_bumped !== true || r.is_checked_out !== false || r.keep_checked_out !== false || r.path !== t.programPath || r.repository !== t.repository || r.version_before !== b.version)
|
|
263
|
+
fail('unknown', 'Check-in result did not establish publication and checkout release.');
|
|
264
|
+
return version(r.version);
|
|
265
|
+
}
|
|
266
|
+
async independent(t, action) {
|
|
267
|
+
const b = await this.launch(t, 'verify-' + randomUUID(), 'reader');
|
|
268
|
+
try {
|
|
269
|
+
const rec = await this.record(t, b);
|
|
270
|
+
await this.boundIdentity(t, rec);
|
|
271
|
+
b.version = await this.localVersion(t, rec);
|
|
272
|
+
return await action(rec, b);
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
await this.stop(t, b);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async published(t, findings) { return this.independent(t, (_r, b) => this.read(t, b, findings)); }
|
|
279
|
+
async probe(t, role) {
|
|
280
|
+
const values = this.context.profile.values;
|
|
281
|
+
const home = text(values.ghidraHome), jar = text(values.mcpJar), java = text(values.java);
|
|
282
|
+
const jars = [jar];
|
|
283
|
+
const collect = async (path) => { for (const item of await readdir(path, { withFileTypes: true })) {
|
|
284
|
+
const child = join(path, item.name);
|
|
285
|
+
if (item.isDirectory())
|
|
286
|
+
await collect(child);
|
|
287
|
+
else if (item.isFile() && child.endsWith('.jar'))
|
|
288
|
+
jars.push(child);
|
|
289
|
+
} };
|
|
290
|
+
await collect(join(home, 'Ghidra'));
|
|
291
|
+
const root = join(this.directory, 'probes', randomUUID());
|
|
292
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
293
|
+
const url = new URL(t.server);
|
|
294
|
+
const password = await this.context.secrets.require(role === 'reader' ? 'readerPassword' : 'publisherPassword');
|
|
295
|
+
const script = fileURLToPath(new URL('../../references/InsightRepositoryProbe.java', import.meta.url));
|
|
296
|
+
let stdout;
|
|
297
|
+
try {
|
|
298
|
+
({ stdout } = await promisify(execFile)(java, ['-Djava.awt.headless=true', '-Dghidra.home=' + home, '-Duser.home=' + root, '-Djava.io.tmpdir=' + root, '-cp', jars.join(delimiter), '--source', '21', script, t.repository, t.programPath, role], {
|
|
299
|
+
cwd: root, timeout: 30000, maxBuffer: 262144, signal: this.context.signal,
|
|
300
|
+
env: { PATH: process.env.PATH, HOME: root, TMPDIR: root, GHIDRA_SERVER_HOST: url.hostname, GHIDRA_SERVER_PORT: url.port || '13100', GHIDRA_SERVER_USER: text(values[role === 'reader' ? 'readerUser' : 'publisherUser']), GHIDRA_SERVER_PASSWORD: password }
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
fail('authorization', 'Authoritative repository probe failed; access and checkout state remain unverified.');
|
|
305
|
+
}
|
|
306
|
+
let result;
|
|
307
|
+
try {
|
|
308
|
+
result = object(JSON.parse(stdout.trim()));
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
fail('malformed', 'Authoritative repository probe returned malformed content.');
|
|
312
|
+
}
|
|
313
|
+
if (result.repository !== t.repository || result.path !== t.programPath || !Array.isArray(result.checkouts) || typeof result.checkinActive !== 'boolean' || !Array.isArray(result.versions))
|
|
314
|
+
fail('binding', 'Authoritative repository probe has incomplete or wrong binding.');
|
|
315
|
+
if (role === 'reader' ? result.permission !== 0 : ![1, 2].includes(Number(result.permission)))
|
|
316
|
+
fail('authorization', 'Actual repository permission does not match the worker role.');
|
|
317
|
+
version(result.version);
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
async history(t, batchId) {
|
|
321
|
+
const result = await this.probe(t, 'reader');
|
|
322
|
+
return result.versions.filter(x => object(x).comment === 'insight batch ' + batchId).map(x => version(object(x).version));
|
|
323
|
+
}
|
|
324
|
+
async released(t) {
|
|
325
|
+
const result = await this.probe(t, 'reader');
|
|
326
|
+
return result.checkouts.length === 0 && result.checkinActive === false;
|
|
327
|
+
}
|
|
328
|
+
async complete(t, b) { if (alive(b.pid))
|
|
329
|
+
await this.stop(t, b); if (b.bridgeSecret)
|
|
330
|
+
await this.context.secrets.delete(b.bridgeSecret); }
|
|
331
|
+
async release(t, b) {
|
|
332
|
+
const rec = await this.record(t, b);
|
|
333
|
+
await rec.http.call('/close_program', { name: t.programPath, save: true });
|
|
334
|
+
const result = object(await rec.http.call('/server/version_control/undo_checkout', { path: t.programPath, keep: false }));
|
|
335
|
+
if (result.success !== true || result.is_checked_out !== false || result.path !== t.programPath)
|
|
336
|
+
fail('checkout', 'Checkout release was not verified.');
|
|
337
|
+
await this.stop(t, b);
|
|
338
|
+
}
|
|
339
|
+
async abandon(t, batch) {
|
|
340
|
+
const receipt = join(this.directory, 'processes', batch.id, 'process.json');
|
|
341
|
+
let binding;
|
|
342
|
+
try {
|
|
343
|
+
binding = JSON.parse(await readFile(receipt, 'utf8'));
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
if (e.code === 'ENOENT' && !batch.binding)
|
|
347
|
+
return;
|
|
348
|
+
throw e;
|
|
349
|
+
}
|
|
350
|
+
if (alive(binding.pid))
|
|
351
|
+
fail('busy', 'The staged publisher is still alive; stop it explicitly before abandoning.');
|
|
352
|
+
assertIdentity(binding.identity, t);
|
|
353
|
+
const backup = join(this.directory, 'backups', batch.id + '-' + randomUUID());
|
|
354
|
+
await cp(binding.projectDirectory, backup, { recursive: true, errorOnExist: true });
|
|
355
|
+
// Automatic offline checkout cancellation would need its actual checkout ID and
|
|
356
|
+
// server quiescence proof. Preserve the backup and require explicit operator cleanup.
|
|
357
|
+
if (!await this.released(t))
|
|
358
|
+
fail('checkout', 'A server checkout remains; the stage and backup are retained for explicit cleanup.');
|
|
359
|
+
}
|
|
360
|
+
async startReader(t, label) {
|
|
361
|
+
const python = text(this.context.profile.values.python), bridgeDirectory = text(this.context.profile.values.bridgeDirectory);
|
|
362
|
+
if (!isAbsolute(python) || !isAbsolute(bridgeDirectory) || !(await stat(python)).isFile() || !(await stat(bridgeDirectory)).isDirectory())
|
|
363
|
+
fail('configuration', 'Reader requires an absolute Python executable and bridge directory.');
|
|
364
|
+
const b = await this.launch(t, 'reader-' + label + '-' + randomUUID(), 'reader');
|
|
365
|
+
try {
|
|
366
|
+
const rec = await this.record(t, b);
|
|
367
|
+
await this.boundIdentity(t, rec);
|
|
368
|
+
b.version = await this.localVersion(t, rec);
|
|
369
|
+
const transport = new StdioClientTransport({ command: python, args: ['-m', 'bridge_mcp_ghidra', '--no-lazy'], cwd: b.projectDirectory, stderr: 'pipe',
|
|
370
|
+
env: { PATH: this.context.environment.PATH ?? process.env.PATH ?? '', HOME: b.projectDirectory, XDG_RUNTIME_DIR: this.directory, PYTHONPATH: bridgeDirectory, GHIDRA_MCP_URL: b.endpoint, GHIDRA_MCP_REQUIRE_PROGRAM_SELECTORS: '1' } });
|
|
371
|
+
transport.stderr?.on('data', () => { });
|
|
372
|
+
const client = new Client({ name: 'insight-ghidra-reader', version: '0.1.0' });
|
|
373
|
+
rec.client = client;
|
|
374
|
+
rec.transport = transport;
|
|
375
|
+
await client.connect(transport);
|
|
376
|
+
const result = object(decodeMcp(await client.callTool({ name: 'connect_instance', arguments: { project: 'endpoint-only-' + randomUUID() } })));
|
|
377
|
+
if (result.connected !== true || result.url !== b.endpoint) {
|
|
378
|
+
await client.close();
|
|
379
|
+
fail('binding', 'MCP bridge connected to a different endpoint.');
|
|
380
|
+
}
|
|
381
|
+
if (!transport.pid)
|
|
382
|
+
fail('binding', 'MCP bridge did not report its process identity.');
|
|
383
|
+
b.bridgePid = transport.pid;
|
|
384
|
+
await durable(join(b.projectDirectory, 'bridge.json'), { pid: transport.pid, endpoint: b.endpoint, workerPid: b.pid });
|
|
385
|
+
return b;
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
await this.abortReader(t, b);
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async abortReader(t, b) {
|
|
393
|
+
const root = await this.receipt(t, b), rec = this.processes.get(root);
|
|
394
|
+
await rec?.client?.close().catch(() => { });
|
|
395
|
+
await rec?.transport?.close().catch(() => { });
|
|
396
|
+
// Only a child handle created by this owner may be cleaned before its endpoint is ready.
|
|
397
|
+
// Recovered bindings continue through stop(), which verifies a live endpoint before signaling.
|
|
398
|
+
if (rec?.child && rec.child.pid === b.pid) {
|
|
399
|
+
if (rec.child.exitCode === null && rec.child.signalCode === null)
|
|
400
|
+
rec.child.kill('SIGTERM');
|
|
401
|
+
for (let i = 0; i < 100 && rec.child.exitCode === null && rec.child.signalCode === null; i++)
|
|
402
|
+
await delay(50);
|
|
403
|
+
if (rec.child.exitCode === null && rec.child.signalCode === null)
|
|
404
|
+
fail('busy', 'Failed reader did not stop; its owned stage is retained.');
|
|
405
|
+
this.processes.delete(root);
|
|
406
|
+
}
|
|
407
|
+
else
|
|
408
|
+
await this.stop(t, b);
|
|
409
|
+
}
|
|
410
|
+
async inspect(t, b, functions) {
|
|
411
|
+
if (b.role !== 'reader')
|
|
412
|
+
fail('authorization', 'Inspection requires a reader worker.');
|
|
413
|
+
const rec = await this.record(t, b);
|
|
414
|
+
await this.boundIdentity(t, rec);
|
|
415
|
+
if (!rec.client)
|
|
416
|
+
fail('workerStopped', 'Reader bridge belongs to a prior owner; stop and restart the worker.');
|
|
417
|
+
const refs = functions.split(',');
|
|
418
|
+
if (!refs.length || refs.length > 8 || refs.some(s => !s))
|
|
419
|
+
fail('invalid', 'Inspect requires one to eight explicit function selectors.');
|
|
420
|
+
const direct = object(await rec.http.call('/get_functions', { functions, fields: 'entry_point', include_call_context: false, program: t.programPath }));
|
|
421
|
+
const directFunctions = object(direct.functions);
|
|
422
|
+
const epoch = text(object(object(directFunctions[refs[0]]).revision).epoch);
|
|
423
|
+
if (b.readEpoch && b.readEpoch !== epoch)
|
|
424
|
+
fail('binding', 'Reader snapshot reopened after its baseline was recorded.');
|
|
425
|
+
const result = object(decodeMcp(await rec.client.callTool({ name: 'get_functions', arguments: { functions, fields: 'entry_point,decompiled_code,comments', include_call_context: false, program: t.programPath } })));
|
|
426
|
+
const returned = object(result.functions);
|
|
427
|
+
if (Object.keys(returned).length !== refs.length || refs.some(ref => !returned[ref] || object(returned[ref]).error))
|
|
428
|
+
fail('partial', 'MCP function result omitted or failed a requested selector.');
|
|
429
|
+
for (const ref of refs) {
|
|
430
|
+
const revision = object(object(returned[ref]).revision);
|
|
431
|
+
if (revision.epoch !== epoch || revision.file_version !== undefined && revision.file_version !== -1 && revision.file_version !== b.version)
|
|
432
|
+
fail('binding', 'Reader baseline or bridge routing changed.');
|
|
433
|
+
}
|
|
434
|
+
if (b.openingVersion !== b.version)
|
|
435
|
+
fail('binding', 'Reader lacks a stable version proof at opening.');
|
|
436
|
+
b.readEpoch = epoch;
|
|
437
|
+
return { binding: b, functions: returned };
|
|
438
|
+
}
|
|
439
|
+
async stop(t, b) {
|
|
440
|
+
const root = await this.receipt(t, b);
|
|
441
|
+
if (!alive(b.pid)) {
|
|
442
|
+
await this.processes.get(root)?.client?.close();
|
|
443
|
+
this.processes.delete(root);
|
|
444
|
+
if (b.bridgeSecret)
|
|
445
|
+
await this.context.secrets.delete(b.bridgeSecret);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const rec = await this.record(t, b);
|
|
449
|
+
await rec.client?.close();
|
|
450
|
+
// Verify the actual receipt/PID/endpoint before signaling; never kill by port/name.
|
|
451
|
+
process.kill(b.pid, 'SIGTERM');
|
|
452
|
+
for (let i = 0; i < 100 && alive(b.pid); i++)
|
|
453
|
+
await delay(50);
|
|
454
|
+
if (alive(b.pid))
|
|
455
|
+
fail('busy', 'Owned Ghidra process did not stop; no forced takeover was attempted.');
|
|
456
|
+
this.processes.delete(b.projectDirectory);
|
|
457
|
+
if (b.bridgeSecret)
|
|
458
|
+
await this.context.secrets.delete(b.bridgeSecret);
|
|
459
|
+
}
|
|
460
|
+
async dispose() {
|
|
461
|
+
for (const rec of [...this.processes.values()]) {
|
|
462
|
+
await rec.client?.close().catch(() => { });
|
|
463
|
+
// Publisher stages survive owner disposal until their outcome is reconciled.
|
|
464
|
+
if (rec.binding.role === 'reader' && alive(rec.binding.pid))
|
|
465
|
+
await this.fork({ ...this.context, signal: new AbortController().signal }).stop(rec.binding.identity, rec.binding);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createDefinition, createInsightCli } from './cli.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Workspace, ResearchTarget } from './workspace.js';
|
|
2
|
+
import type { GhidraServer, ServerBinding } from './server.js';
|
|
3
|
+
export declare const initialRoot: (workspace: Workspace, target: string) => string;
|
|
4
|
+
export declare function initialIntent(workspace: Workspace, target: string): Promise<Record<string, unknown> | undefined>;
|
|
5
|
+
/** Immutable input/authority binding; private attempts and conditional first creation share it. */
|
|
6
|
+
export declare function initializeModel(workspace: Workspace, target: ResearchTarget, binding: ServerBinding, server: Pick<GhidraServer, 'initialImport' | 'initialPublication'>, fingerprint: string): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { mkdir, realpath, readFile, open, link, unlink } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { stable, object, fail } from './model.js';
|
|
5
|
+
export const initialRoot = (workspace, target) => join(workspace.directory, 'ghidra-native', 'initial', target);
|
|
6
|
+
export async function initialIntent(workspace, target) {
|
|
7
|
+
try {
|
|
8
|
+
return object(JSON.parse(await readFile(join(initialRoot(workspace, target), 'intent.json'), 'utf8')));
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error.code === 'ENOENT')
|
|
12
|
+
return;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Immutable input/authority binding; private attempts and conditional first creation share it. */
|
|
17
|
+
export async function initializeModel(workspace, target, binding, server, fingerprint) {
|
|
18
|
+
const root = initialRoot(workspace, target.id);
|
|
19
|
+
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
20
|
+
if (await realpath(root) !== resolve(root))
|
|
21
|
+
fail('binding', 'Initial model storage moved outside its owned root.');
|
|
22
|
+
const expected = { schema: 1, target: target.id, sha256: target.binarySha256, input: await workspace.artifacts.verify(target.input),
|
|
23
|
+
origin: binding.origin, repository: binding.repository, readerUser: binding.readerUser, publisherUser: binding.publisherUser,
|
|
24
|
+
settings: workspace.model(target.id).settings, runtimeFingerprint: fingerprint };
|
|
25
|
+
let intent = await initialIntent(workspace, target.id);
|
|
26
|
+
if (!intent) {
|
|
27
|
+
const temporary = join(root, 'intent.new-' + randomUUID()), handle = await open(temporary, 'wx', 0o600);
|
|
28
|
+
try {
|
|
29
|
+
await handle.writeFile(stable({ ...expected, id: randomUUID() }));
|
|
30
|
+
await handle.sync();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await handle.close();
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
await link(temporary, join(root, 'intent.json'));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code !== 'EEXIST')
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await unlink(temporary);
|
|
44
|
+
}
|
|
45
|
+
if (process.platform !== 'win32') {
|
|
46
|
+
const directory = await open(root, 'r');
|
|
47
|
+
try {
|
|
48
|
+
await directory.sync();
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await directory.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
intent = await initialIntent(workspace, target.id);
|
|
55
|
+
}
|
|
56
|
+
if (!intent || typeof intent.id !== 'string' || !/^[a-f0-9-]{36}$/.test(intent.id) ||
|
|
57
|
+
Object.entries(expected).some(([key, value]) => stable(intent[key]) !== stable(value)))
|
|
58
|
+
fail('binding', 'Retained initial model differs from its input, load settings, runtime or repository authority.');
|
|
59
|
+
if (workspace.model(target.id).state === 'opening')
|
|
60
|
+
workspace.beginModelInitialization(target.id);
|
|
61
|
+
const prepared = await server.initialImport(root);
|
|
62
|
+
if (prepared.state === 'busy')
|
|
63
|
+
fail('busy', 'The original initial-import worker is still active; retain the context and continue after it completes.');
|
|
64
|
+
if (prepared.state !== 'prepared')
|
|
65
|
+
fail('unknown', 'Initial import did not establish a complete private snapshot.');
|
|
66
|
+
const receipt = object(JSON.parse(await readFile(join(root, 'import.json'), 'utf8'))), name = receipt.snapshot;
|
|
67
|
+
if (typeof name !== 'string' || !/^attempt-[a-f0-9-]{36}\.gzf$/.test(name))
|
|
68
|
+
fail('binding', 'Initial model snapshot receipt is malformed.');
|
|
69
|
+
const snapshot = await workspace.captureSnapshot(join(root, name));
|
|
70
|
+
if (snapshot.sha256 !== receipt.snapshotSha256)
|
|
71
|
+
fail('binding', 'Initial saved snapshot changed before publication.');
|
|
72
|
+
let result = await server.initialPublication(binding, root, 'stage');
|
|
73
|
+
if (result.state === 'staged') {
|
|
74
|
+
const proof = await server.initialPublication(binding, root, 'verify-stage');
|
|
75
|
+
if (proof.state !== 'stage_verified' || proof.version !== 1 || proof.snapshotBuffersVerified !== true)
|
|
76
|
+
fail('unknown', 'Independent initial staging readback did not establish the complete snapshot.');
|
|
77
|
+
result = await server.initialPublication(binding, root, 'promote');
|
|
78
|
+
}
|
|
79
|
+
if (result.state !== 'published' || result.version !== 1 || result.snapshotBuffersVerified !== true)
|
|
80
|
+
fail('unknown', 'Initial publication requires reconciliation against its saved snapshot.');
|
|
81
|
+
const verified = await server.initialPublication(binding, root, 'verify');
|
|
82
|
+
if (verified.state !== 'verified' || verified.version !== 1 || verified.snapshotBuffersVerified !== true)
|
|
83
|
+
fail('unknown', 'Independent initial-model readback did not establish the complete saved buffers.');
|
|
84
|
+
workspace.establishModel(target.id, 1, snapshot);
|
|
85
|
+
}
|
package/dist/src/json.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Preserve exact integer values that cannot be represented by a JavaScript number. */
|
|
2
|
+
export function parseDomainJson(source) {
|
|
3
|
+
return JSON.parse(source, (_key, value, context) => {
|
|
4
|
+
if (typeof value === 'number' && Number.isInteger(value) && !Number.isSafeInteger(value) && context?.source && /^-?\d+$/.test(context.source))
|
|
5
|
+
return context.source;
|
|
6
|
+
return value;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
import { Research, type ResearchBackend } from './research.js';
|
|
3
|
+
import { Workspace, type Scope } from './workspace.js';
|
|
4
|
+
export declare class Lifecycle {
|
|
5
|
+
private readonly workspace;
|
|
6
|
+
private readonly research;
|
|
7
|
+
private readonly backend;
|
|
8
|
+
constructor(workspace: Workspace, research: Research, backend: () => Promise<ResearchBackend>);
|
|
9
|
+
health(selected: Scope): Promise<{
|
|
10
|
+
health: {
|
|
11
|
+
connected: boolean;
|
|
12
|
+
mode: unknown;
|
|
13
|
+
version: unknown;
|
|
14
|
+
memory: unknown;
|
|
15
|
+
};
|
|
16
|
+
externalRequests: {
|
|
17
|
+
operation: string;
|
|
18
|
+
state: import("./workspace.js").RequestState;
|
|
19
|
+
requestId?: string | undefined;
|
|
20
|
+
}[];
|
|
21
|
+
externalRequestsTruncated: boolean;
|
|
22
|
+
ghidra: {
|
|
23
|
+
syncPolicy: import("./workspace.js").SyncPolicy;
|
|
24
|
+
state: "opening" | "ready" | "stopped" | "needs_recovery";
|
|
25
|
+
baseRevision: string | undefined;
|
|
26
|
+
sharedRevision: string | undefined;
|
|
27
|
+
pendingChanges: number;
|
|
28
|
+
mergeState: string;
|
|
29
|
+
conflicts: {
|
|
30
|
+
address: string;
|
|
31
|
+
field: "function.name" | "comment.eol" | "comment.pre" | "comment.post" | "comment.plate" | "comment.repeatable";
|
|
32
|
+
original: string | null;
|
|
33
|
+
local: string | null;
|
|
34
|
+
shared: string | null;
|
|
35
|
+
}[];
|
|
36
|
+
unresolvedRequests: {
|
|
37
|
+
operation: string;
|
|
38
|
+
state: import("./workspace.js").RequestState;
|
|
39
|
+
requestId?: string | undefined;
|
|
40
|
+
}[];
|
|
41
|
+
};
|
|
42
|
+
session: string;
|
|
43
|
+
target: string;
|
|
44
|
+
}>;
|
|
45
|
+
change(selected: Scope, operation: 'save' | 'close', callerId?: string): Promise<unknown>;
|
|
46
|
+
discard(selected: Scope, callerId?: string): Promise<unknown>;
|
|
47
|
+
saveAll(session: string, callerId?: string): Promise<unknown>;
|
|
48
|
+
}
|
|
49
|
+
export declare function lifecycleCommands(service: (context: CommandContext) => Lifecycle): CommandDefinition[];
|