@eyeauras/insight-cli 0.0.0-stage → 0.2.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 +60 -2
- package/SKILL.md +58 -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 +134 -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 +419 -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 +1078 -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 +154 -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 +182 -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/runtime-archives.d.ts +5 -0
- package/dist/src/runtime-archives.js +127 -0
- package/dist/src/runtime-catalog.d.ts +18 -0
- package/dist/src/runtime-catalog.js +27 -0
- package/dist/src/runtime-manager.d.ts +101 -0
- package/dist/src/runtime-manager.js +444 -0
- package/dist/src/runtime-selection.d.ts +13 -0
- package/dist/src/runtime-selection.js +25 -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 +46 -0
- package/dist/src/server.js +460 -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 +47 -4
- package/references/InsightRepositoryProbe.java +55 -0
- package/references/administration.md +34 -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/managed-runtime.md +116 -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/InsightRuntimeDoctor.java +33 -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 +49 -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,196 @@
|
|
|
1
|
+
import { command, CliError, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { lstat, mkdir, open, opendir, realpath, rename, rm } from 'node:fs/promises';
|
|
5
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
const maximumEntries = 100_000, maximumDepth = 128;
|
|
7
|
+
function fail(code, message) { throw new CliError(message, { code: 'artifacts.' + code }); }
|
|
8
|
+
function check(signal) { if (signal?.aborted)
|
|
9
|
+
fail('cancelled', 'Saving materials was cancelled; no snapshot was published.'); }
|
|
10
|
+
function inside(parent, child) {
|
|
11
|
+
const value = relative(parent, child);
|
|
12
|
+
return value === '' || (!isAbsolute(value) && value !== '..' && !value.startsWith('..' + sep));
|
|
13
|
+
}
|
|
14
|
+
function stamp(info) { return [info.dev, info.ino, info.mode, info.size, info.mtimeNs, info.ctimeNs].join(':'); }
|
|
15
|
+
async function syncDirectory(path) {
|
|
16
|
+
if (process.platform === 'win32')
|
|
17
|
+
return;
|
|
18
|
+
const handle = await open(path, constants.O_RDONLY);
|
|
19
|
+
try {
|
|
20
|
+
await handle.sync();
|
|
21
|
+
}
|
|
22
|
+
finally {
|
|
23
|
+
await handle.close();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function inventory(source, signal) {
|
|
27
|
+
const entries = [];
|
|
28
|
+
async function visit(path, name, depth) {
|
|
29
|
+
check(signal);
|
|
30
|
+
if (depth > maximumDepth || entries.length >= maximumEntries)
|
|
31
|
+
fail('limit', 'Materials exceed the traversal bound of 100000 entries or 128 directory levels; save a smaller selection.');
|
|
32
|
+
const info = await lstat(path, { bigint: true });
|
|
33
|
+
if (!info.isFile() && !info.isDirectory())
|
|
34
|
+
fail('unsupported', 'Materials must contain only ordinary files and directories; symbolic links and special files are unsupported.');
|
|
35
|
+
if (info.size > BigInt(Number.MAX_SAFE_INTEGER))
|
|
36
|
+
fail('limit', 'A materials entry exceeds the supported size counter.');
|
|
37
|
+
entries.push({ path: name, kind: info.isFile() ? 'file' : 'directory', stamp: stamp(info), size: Number(info.size) });
|
|
38
|
+
if (info.isDirectory()) {
|
|
39
|
+
// Sorting by code unit keeps manifest digests independent of locale and enumeration order.
|
|
40
|
+
const names = [];
|
|
41
|
+
for await (const child of await opendir(path)) {
|
|
42
|
+
check(signal);
|
|
43
|
+
if (names.length + entries.length >= maximumEntries)
|
|
44
|
+
fail('limit', 'Materials exceed the traversal bound of 100000 entries; save a smaller selection.');
|
|
45
|
+
names.push(child.name);
|
|
46
|
+
}
|
|
47
|
+
names.sort();
|
|
48
|
+
for (const child of names)
|
|
49
|
+
await visit(join(path, child), name ? name + '/' + child : child, depth + 1);
|
|
50
|
+
if (stamp(await lstat(path, { bigint: true })) !== stamp(info))
|
|
51
|
+
fail('changed', 'Source changed while saving; save a stable file or directory.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
await visit(source, '', 0);
|
|
55
|
+
return entries;
|
|
56
|
+
}
|
|
57
|
+
async function verifyParents(source, entry, entries) {
|
|
58
|
+
let parent = entry.path ? entry.path.split('/').slice(0, -1).join('/') : undefined;
|
|
59
|
+
while (parent !== undefined) {
|
|
60
|
+
const expected = entries.get(parent);
|
|
61
|
+
if (stamp(await lstat(parent ? join(source, parent) : source, { bigint: true })) !== expected.stamp)
|
|
62
|
+
fail('changed', 'Source directory changed while saving; no snapshot was published.');
|
|
63
|
+
parent = parent ? parent.split('/').slice(0, -1).join('/') : undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Copy user materials without initializing Ghidra, targets, sessions or Workspace. */
|
|
67
|
+
export async function saveMaterials(appDataDirectory, source, signal) {
|
|
68
|
+
let stage, published;
|
|
69
|
+
try {
|
|
70
|
+
check(signal);
|
|
71
|
+
const sourcePath = resolve(source);
|
|
72
|
+
// Reject a symbolic source itself before resolving its parents (e.g. macOS /tmp).
|
|
73
|
+
const info = await lstat(sourcePath, { bigint: true });
|
|
74
|
+
if (!info.isFile() && !info.isDirectory())
|
|
75
|
+
fail('unsupported', 'Save an ordinary file or directory; symbolic links and special files are unsupported.');
|
|
76
|
+
const canonicalSource = await realpath(sourcePath);
|
|
77
|
+
await mkdir(appDataDirectory, { recursive: true, mode: 0o700 });
|
|
78
|
+
const appData = await realpath(appDataDirectory), storage = join(appData, 'materials');
|
|
79
|
+
if (info.isDirectory() && inside(canonicalSource, storage))
|
|
80
|
+
fail('recursive', 'Source contains the materials storage; choose a source outside its storage ancestry.');
|
|
81
|
+
await mkdir(storage, { recursive: true, mode: 0o700 });
|
|
82
|
+
if ((await lstat(storage)).isSymbolicLink() || await realpath(storage) !== storage)
|
|
83
|
+
fail('storage', 'Materials storage must be an ordinary profile-owned directory.');
|
|
84
|
+
const entries = await inventory(canonicalSource, signal), byPath = new Map(entries.map(entry => [entry.path, entry]));
|
|
85
|
+
if (entries[0].stamp !== stamp(info))
|
|
86
|
+
fail('changed', 'Source changed while preparing its snapshot; save a stable file or directory.');
|
|
87
|
+
const identity = randomUUID(), candidate = join(storage, '.pending-' + identity);
|
|
88
|
+
const final = join(storage, identity), content = join(candidate, 'content');
|
|
89
|
+
await mkdir(candidate, { mode: 0o700 });
|
|
90
|
+
stage = candidate;
|
|
91
|
+
await mkdir(content, { mode: 0o700 });
|
|
92
|
+
const name = basename(canonicalSource) || 'root', output = join(content, name);
|
|
93
|
+
const manifest = [], directories = [stage, content];
|
|
94
|
+
let files = 0, bytes = 0, fileDigest;
|
|
95
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
check(signal);
|
|
98
|
+
await verifyParents(canonicalSource, entry, byPath);
|
|
99
|
+
const inputPath = entry.path ? join(canonicalSource, entry.path) : canonicalSource;
|
|
100
|
+
const outputPath = entry.path ? join(output, entry.path) : output;
|
|
101
|
+
if (entry.kind === 'directory') {
|
|
102
|
+
await mkdir(outputPath, { mode: 0o700 });
|
|
103
|
+
directories.push(outputPath);
|
|
104
|
+
manifest.push({ path: entry.path, kind: 'directory' });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// NONBLOCK avoids a hang if a regular source is exchanged for a FIFO before open.
|
|
108
|
+
const input = await open(inputPath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
109
|
+
try {
|
|
110
|
+
const before = await input.stat({ bigint: true });
|
|
111
|
+
if (!before.isFile() || stamp(before) !== entry.stamp)
|
|
112
|
+
fail('changed', 'Source file changed while saving; no snapshot was published.');
|
|
113
|
+
const destination = await open(outputPath, 'wx', 0o600), hash = createHash('sha256');
|
|
114
|
+
let copied = 0;
|
|
115
|
+
try {
|
|
116
|
+
while (true) {
|
|
117
|
+
check(signal);
|
|
118
|
+
const { bytesRead } = await input.read(buffer, 0, buffer.length, copied);
|
|
119
|
+
if (!bytesRead)
|
|
120
|
+
break;
|
|
121
|
+
copied += bytesRead;
|
|
122
|
+
if (copied > entry.size)
|
|
123
|
+
fail('changed', 'Source file grew while saving; no snapshot was published.');
|
|
124
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
125
|
+
hash.update(chunk);
|
|
126
|
+
await destination.writeFile(chunk);
|
|
127
|
+
}
|
|
128
|
+
if (copied !== entry.size || stamp(await input.stat({ bigint: true })) !== entry.stamp ||
|
|
129
|
+
stamp(await lstat(inputPath, { bigint: true })) !== entry.stamp)
|
|
130
|
+
fail('changed', 'Source file changed while saving; no snapshot was published.');
|
|
131
|
+
await destination.sync();
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await destination.close();
|
|
135
|
+
}
|
|
136
|
+
const sha256 = hash.digest('hex');
|
|
137
|
+
manifest.push({ path: entry.path, kind: 'file', bytes: copied, sha256 });
|
|
138
|
+
fileDigest = sha256;
|
|
139
|
+
files++;
|
|
140
|
+
bytes += copied;
|
|
141
|
+
if (!Number.isSafeInteger(bytes))
|
|
142
|
+
fail('limit', 'Materials exceed the supported size counter.');
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
await input.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Detect edits to earlier files and directory membership changes during the entire capture.
|
|
149
|
+
const after = await inventory(canonicalSource, signal);
|
|
150
|
+
if (JSON.stringify(after) !== JSON.stringify(entries) || await realpath(sourcePath) !== canonicalSource ||
|
|
151
|
+
stamp(await lstat(sourcePath, { bigint: true })) !== stamp(info))
|
|
152
|
+
fail('changed', 'Source changed while saving; no snapshot was published.');
|
|
153
|
+
const manifestBytes = JSON.stringify({ version: 1, entries: manifest }) + '\n';
|
|
154
|
+
const metadata = await open(join(stage, 'manifest.json'), 'wx', 0o600);
|
|
155
|
+
try {
|
|
156
|
+
await metadata.writeFile(manifestBytes);
|
|
157
|
+
await metadata.sync();
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await metadata.close();
|
|
161
|
+
}
|
|
162
|
+
for (const directory of directories.reverse())
|
|
163
|
+
await syncDirectory(directory);
|
|
164
|
+
check(signal);
|
|
165
|
+
await rename(stage, final);
|
|
166
|
+
published = final;
|
|
167
|
+
stage = undefined;
|
|
168
|
+
await syncDirectory(storage);
|
|
169
|
+
return { state: 'saved', path: join(final, 'content', name), kind: entries[0].kind, files, bytes,
|
|
170
|
+
sha256: entries[0].kind === 'file' ? fileDigest : createHash('sha256').update(manifestBytes).digest('hex') };
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
let cleanupFailed = false;
|
|
174
|
+
if (stage)
|
|
175
|
+
await rm(stage, { recursive: true, force: true }).catch(() => { cleanupFailed = true; });
|
|
176
|
+
// A directory-sync failure is not acknowledged as a durable save.
|
|
177
|
+
if (published)
|
|
178
|
+
await rm(published, { recursive: true, force: true }).catch(() => { cleanupFailed = true; });
|
|
179
|
+
if (error instanceof CliError) {
|
|
180
|
+
if (cleanupFailed)
|
|
181
|
+
throw new CliError(error.message + ' Some owned temporary files could not be removed; preserve the profile for cleanup.', { code: error.code });
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
const code = error.code;
|
|
185
|
+
fail('save', `Could not save materials${code && /^[A-Z0-9_]+$/.test(code) ? ' (' + code + ')' : ''}; no complete snapshot was acknowledged.${cleanupFailed ? ' Some owned temporary files could not be removed; preserve the profile for cleanup.' : ''}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export function artifactsCommands() {
|
|
189
|
+
return [command('artifacts', 'Retain ordinary research files independently of analysis tools', [
|
|
190
|
+
command('save <path>', 'Save a complete independent copy of a file or directory in this profile', ({ args }, context) => {
|
|
191
|
+
if (typeof args.path !== 'string' || !args.path || args.path.includes('\0'))
|
|
192
|
+
fail('invalid', 'Provide one file or directory path.');
|
|
193
|
+
return saveMaterials(context.appArguments.AppDataDirectory, resolve(context.cwd, args.path), context.signal);
|
|
194
|
+
}, { permission: Permission.Update }),
|
|
195
|
+
])];
|
|
196
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ScriptItem {
|
|
2
|
+
script: string;
|
|
3
|
+
name: string;
|
|
4
|
+
language: 'Java' | 'Python';
|
|
5
|
+
origin: 'installation' | 'context' | 'extension';
|
|
6
|
+
category?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ScriptPage {
|
|
9
|
+
items: ScriptItem[];
|
|
10
|
+
count: number;
|
|
11
|
+
total: number;
|
|
12
|
+
offset: number;
|
|
13
|
+
limit: number;
|
|
14
|
+
truncated: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Resolve only an explicit inventory ID; native search paths never choose another script. */
|
|
17
|
+
export declare function scriptPath(home: string, contextRoot: string, script: string, extensions?: string[]): Promise<string>;
|
|
18
|
+
/** Enumerate configured installation and owned context scripts without exposing native paths. */
|
|
19
|
+
export declare function scriptInventory(home: string, contextRoot: string, filter: string, limit: number, offset: number, extensions?: string[]): Promise<ScriptPage>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readdir, open } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, sep } from 'node:path';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { fail } from './model.js';
|
|
5
|
+
/** Resolve only an explicit inventory ID; native search paths never choose another script. */
|
|
6
|
+
export async function scriptPath(home, contextRoot, script, extensions = []) {
|
|
7
|
+
if (!/^(installation|context|extension):[^\0]+\.(java|py)$/.test(script) || script.includes('..') || script.includes('\\'))
|
|
8
|
+
fail('invalid', 'Choose an explicit script ID returned by ghidra scripts list.');
|
|
9
|
+
const page = await scriptInventory(home, contextRoot, script, 20000, 0, extensions), item = page.items.find(item => item.script === script);
|
|
10
|
+
if (!item)
|
|
11
|
+
fail('notFound', 'Script ID is absent from this context and installation.');
|
|
12
|
+
const part = script.slice(script.indexOf(':') + 1);
|
|
13
|
+
if (item.origin === 'extension') {
|
|
14
|
+
const [module, ...rest] = part.split('/'), root = extensions.find(p => p.split(/[\\/]/).pop() === module);
|
|
15
|
+
if (!root)
|
|
16
|
+
fail('binding', 'Script module is no longer enabled.');
|
|
17
|
+
return join(root, 'ghidra_scripts', ...rest);
|
|
18
|
+
}
|
|
19
|
+
if (item.origin === 'context')
|
|
20
|
+
return join(contextRoot, 'ghidra_scripts', part);
|
|
21
|
+
const segments = part.split('/'), filename = segments.pop();
|
|
22
|
+
return join(home, 'Ghidra', ...segments, 'ghidra_scripts', filename);
|
|
23
|
+
}
|
|
24
|
+
/** Enumerate configured installation and owned context scripts without exposing native paths. */
|
|
25
|
+
export async function scriptInventory(home, contextRoot, filter, limit, offset, extensions = []) {
|
|
26
|
+
const items = [];
|
|
27
|
+
async function scripts(folder, origin, module) {
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = await readdir(folder, { withFileTypes: true });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code === 'ENOENT')
|
|
34
|
+
return;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
38
|
+
if (!entry.isFile() || !entry.name.endsWith('.java') && !entry.name.endsWith('.py'))
|
|
39
|
+
continue;
|
|
40
|
+
if (items.length >= 20000)
|
|
41
|
+
fail('limit', 'Script inventory exceeds 20000 entries.');
|
|
42
|
+
const name = entry.name, script = origin + ':' + (module ? module + '/' : '') + name;
|
|
43
|
+
const handle = await open(join(folder, name), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
44
|
+
let category;
|
|
45
|
+
try {
|
|
46
|
+
const data = Buffer.alloc(4096), read = await handle.read(data, 0, data.length, 0);
|
|
47
|
+
if (!(await handle.stat()).isFile())
|
|
48
|
+
fail('binding', 'Script inventory file changed during inspection.');
|
|
49
|
+
category = /^\s*(?:\/\/|#)\s*@category\s+([^\r\n]+)/m.exec(data.subarray(0, read.bytesRead).toString('utf8'))?.[1]?.trim().slice(0, 256);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
await handle.close();
|
|
53
|
+
}
|
|
54
|
+
items.push({ script, name, language: name.endsWith('.java') ? 'Java' : 'Python', origin, ...(category ? { category } : {}) });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const installed = join(home, 'Ghidra');
|
|
58
|
+
async function modules(folder, depth) {
|
|
59
|
+
if (depth > 16)
|
|
60
|
+
fail('limit', 'Script module nesting exceeds 16 levels.');
|
|
61
|
+
for (const entry of (await readdir(folder, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
62
|
+
if (!entry.isDirectory())
|
|
63
|
+
continue;
|
|
64
|
+
const path = join(folder, entry.name);
|
|
65
|
+
if (entry.name === 'ghidra_scripts')
|
|
66
|
+
await scripts(path, 'installation', relative(installed, folder).split(sep).join('/'));
|
|
67
|
+
else if (!['data', 'lib', 'os', 'src', 'docs'].includes(entry.name))
|
|
68
|
+
await modules(path, depth + 1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await modules(installed, 0);
|
|
72
|
+
for (const root of extensions)
|
|
73
|
+
await scripts(join(root, 'ghidra_scripts'), 'extension', root.split(/[\\/]/).pop());
|
|
74
|
+
await scripts(join(contextRoot, 'ghidra_scripts'), 'context', '');
|
|
75
|
+
const matched = items.filter(item => !filter || [item.name, item.script, item.category ?? ''].some(value => value.toLowerCase().includes(filter.toLowerCase())))
|
|
76
|
+
.sort((a, b) => a.script.localeCompare(b.script));
|
|
77
|
+
const page = matched.slice(offset, offset + limit);
|
|
78
|
+
return { items: page, count: page.length, total: matched.length, offset, limit, truncated: offset + page.length < matched.length };
|
|
79
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Parameter } from './arguments.js';
|
|
2
|
+
interface SdkOperation {
|
|
3
|
+
operation: string;
|
|
4
|
+
group: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
write?: boolean;
|
|
8
|
+
pair?: boolean;
|
|
9
|
+
params: Parameter[];
|
|
10
|
+
}
|
|
11
|
+
export declare const sdkOperations: SdkOperation[];
|
|
12
|
+
export declare const sdkWrites: Set<string>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const str = (name, required = true) => ({ name, type: 'string', required });
|
|
2
|
+
const at = (name = 'address') => ({ ...str(name), semantic: 'address' });
|
|
3
|
+
const json = (name, type = 'object', required = true) => ({ name, type, required });
|
|
4
|
+
const count = (name, value) => ({ name, type: 'integer', required: false, default: value });
|
|
5
|
+
const empty = (name) => ({ ...str(name, false), default: '', allowEmpty: true });
|
|
6
|
+
const bool = (name) => ({ name, type: 'boolean', required: false, default: 'false' });
|
|
7
|
+
const original = [str('name'), at('expect_start'), { ...count('expect_size', '1'), required: true }, str('expect_sha256')];
|
|
8
|
+
export const sdkOperations = [
|
|
9
|
+
{ operation: 'type-layout', group: 'types', name: 'layout', description: 'Read native packing, alignment, bitfields, settings and source archive identity', params: [str('path')] },
|
|
10
|
+
{ operation: 'type-structure', group: 'types', name: 'define-layout', description: 'Create a native structure with packing/alignment/bitfields; replace only an explicitly selected empty placeholder', write: true, params: [str('path'), json('fields', 'array'), { name: 'packing', type: 'integer', required: false, default: '0' }, { name: 'alignment', type: 'integer', required: false, default: '0' }, bool('replace_placeholder')] },
|
|
11
|
+
{ operation: 'type-settings', group: 'types', name: 'set-settings', description: 'Edit default settings by actual native definition names; null clears an explicit setting', write: true, params: [str('path'), json('settings')] },
|
|
12
|
+
{ operation: 'type-archive-import', group: 'types', name: 'archive-import', description: 'Import explicit GDT type paths with archive identity and saved layout verification', write: true, params: [str('input'), json('types', 'array')] },
|
|
13
|
+
{ operation: 'changes', group: 'changes', name: 'compare', description: 'Categorized native ProgramDiff of two saved private contexts; does not merge', pair: true, params: [{ ...json('categories', 'array', false), default: '["bytes","comments","symbols","functions"]' }, count('max_ranges', '100'), count('timeout_seconds', '30')] },
|
|
14
|
+
{ operation: 'block-state', group: 'memory', name: 'block-state', description: 'Read exact block layout and bounded full-content digest', params: [str('name')] },
|
|
15
|
+
{ operation: 'assembly-preview', group: 'assembly', name: 'preview', description: 'Assemble in this program language and processor context without writing', params: [at(), json('instructions', 'array')] },
|
|
16
|
+
{ operation: 'patch-bytes', group: 'memory', name: 'patch-bytes', description: 'Replace equal-length bytes only when the expected bytes match; clears overlapping listing', write: true, params: [at(), str('expect_hex'), str('bytes_hex')] },
|
|
17
|
+
{ operation: 'assemble', group: 'assembly', name: 'apply', description: 'Apply context-aware assembly with exact byte preconditions; clears overlapping listing', write: true, params: [at(), str('expect_hex'), json('instructions', 'array')] },
|
|
18
|
+
{ operation: 'split-block', group: 'memory', name: 'split-block', description: 'Split one initialized block while verifying all original bytes and layout', write: true, params: [...original, at('split_address'), str('new_name')] },
|
|
19
|
+
{ operation: 'move-block', group: 'memory', name: 'move-block', description: 'Move a block only when its original layout and bytes match', write: true, params: [...original, at()] },
|
|
20
|
+
{ operation: 'join-blocks', group: 'memory', name: 'join-blocks', description: 'Join adjacent compatible blocks with independent content and size preconditions', write: true, params: [str('name'), str('second_name'), str('expect_sha256'), str('second_sha256'), { ...count('expect_size', '1'), required: true }, { ...count('second_size', '1'), required: true }] },
|
|
21
|
+
{ operation: 'context-read', group: 'processor-context', name: 'read', description: 'Read bounded register-value ranges in the saved program context', params: [str('register'), at(), { ...at('end_address'), required: false }, count('limit', '100')] },
|
|
22
|
+
{ operation: 'context-set', group: 'processor-context', name: 'set', description: 'Set a processor register value over at most 65536 bytes; requires unchanged-base publication', write: true, params: [str('register'), at(), at('end_address'), str('value')] },
|
|
23
|
+
{ operation: 'context-clear', group: 'processor-context', name: 'clear', description: 'Remove an explicit processor register value over at most 65536 bytes', write: true, params: [str('register'), at(), at('end_address')] },
|
|
24
|
+
{ operation: 'namespace-list', group: 'namespaces', name: 'list', description: 'List direct namespace/class children with explicit visited-symbol budget', params: [empty('parent'), count('limit', '100'), count('max_scan', '10000')] },
|
|
25
|
+
{ operation: 'namespace-create', group: 'namespaces', name: 'create', description: 'Create an explicit namespace or class below an existing parent', write: true, params: [empty('parent'), str('name'), bool('class')] },
|
|
26
|
+
{ operation: 'namespace-update', group: 'namespaces', name: 'update', description: 'Rename or reparent an ordinary namespace/class without recursive deletion', write: true, params: [str('path'), str('new_name'), empty('parent')] },
|
|
27
|
+
{ operation: 'namespace-delete', group: 'namespaces', name: 'delete', description: 'Delete one empty ordinary namespace/class', write: true, params: [str('path')] },
|
|
28
|
+
{ operation: 'external-list', group: 'externals', name: 'list', description: 'Read library identities or structured locations in one explicit library', params: [empty('library'), count('limit', '100')] },
|
|
29
|
+
{ operation: 'library-create', group: 'externals', name: 'create-library', description: 'Create a named external library in the private program', write: true, params: [str('name')] },
|
|
30
|
+
{ operation: 'library-update', group: 'externals', name: 'update-library', description: 'Rename an external library or change its logical project path', write: true, params: [str('name'), str('new_name', false), str('path', false)] },
|
|
31
|
+
{ operation: 'library-delete', group: 'externals', name: 'delete-library', description: 'Delete an empty external library', write: true, params: [str('name')] },
|
|
32
|
+
{ operation: 'external-create', group: 'externals', name: 'create-location', description: 'Create an external label/function with an optional memory address', write: true, params: [str('library'), str('label'), { ...at(), required: false }, bool('function')] },
|
|
33
|
+
{ operation: 'external-update', group: 'externals', name: 'update-location', description: 'Rename/update one unambiguous external location', write: true, params: [str('library'), str('label'), str('new_label'), { ...at(), required: false }] },
|
|
34
|
+
{ operation: 'external-delete', group: 'externals', name: 'delete-location', description: 'Delete one unambiguous external location, refusing retained dependencies', write: true, params: [str('library'), str('label')] },
|
|
35
|
+
{ operation: 'decompiler-ir', group: 'decompiler', name: 'ir', description: 'Read bounded typed SSA P-code with explicit simplification/options and completion diagnostics', params: [at(), { ...str('simplification', false), default: 'decompile' }, count('max_ops', '1000'), count('timeout_seconds', '30'), bool('c_code'), { ...json('options', 'object', false), default: '{}' }] },
|
|
36
|
+
];
|
|
37
|
+
export const sdkWrites = new Set(sdkOperations.filter(v => v.write).map(v => '/sdk/' + v.operation));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
import { Workspace, type Scope } from './workspace.js';
|
|
3
|
+
import { Research, type ResearchBackend } from './research.js';
|
|
4
|
+
/** Native SDK file effects have retained input/output bytes and never repeat an uncertain call. */
|
|
5
|
+
export declare class SdkFiles {
|
|
6
|
+
private readonly workspace;
|
|
7
|
+
private readonly research;
|
|
8
|
+
private readonly backend;
|
|
9
|
+
constructor(workspace: Workspace, research: Research, backend: () => Promise<ResearchBackend>);
|
|
10
|
+
loaders(input: string): Promise<{
|
|
11
|
+
inputSha256: string;
|
|
12
|
+
}>;
|
|
13
|
+
exporters(selected: Scope): Promise<{
|
|
14
|
+
value: Record<string, unknown>;
|
|
15
|
+
session: string;
|
|
16
|
+
target: string;
|
|
17
|
+
}>;
|
|
18
|
+
private copy;
|
|
19
|
+
private completedOutput;
|
|
20
|
+
databaseQuery(selected: Scope, operation: 'fid-query' | 'bsim-query', database: string, p: Record<string, unknown>): Promise<{
|
|
21
|
+
databaseSha256: string;
|
|
22
|
+
sourceSnapshotSha256: string;
|
|
23
|
+
value: Record<string, unknown>;
|
|
24
|
+
session: string;
|
|
25
|
+
target: string;
|
|
26
|
+
}>;
|
|
27
|
+
export(selected: Scope, operation: 'export' | 'archive-export' | 'fid-build' | 'bsim-build', parameters: Record<string, unknown>, directory: string, callerId?: string): Promise<unknown>;
|
|
28
|
+
}
|
|
29
|
+
export declare function sdkFileCommands(service: (c: CommandContext) => SdkFiles): CommandDefinition[];
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { command, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { mkdir, readdir, lstat, readFile, open, rename, copyFile, realpath, chmod } from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { join, resolve, dirname } from 'node:path';
|
|
5
|
+
import { scope, address, number } from './research.js';
|
|
6
|
+
import { object, fail, text, id, stable } from './model.js';
|
|
7
|
+
import { parseDomainJson } from './json.js';
|
|
8
|
+
import { selectedOptions } from './arguments.js';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
async function persist(path, value) { const temp = path + '.new-' + randomUUID(), file = await open(temp, 'wx', 0o600); try {
|
|
11
|
+
await file.writeFile(stable(value));
|
|
12
|
+
await file.sync();
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
await file.close();
|
|
16
|
+
} await rename(temp, path); if (process.platform !== 'win32') {
|
|
17
|
+
const directory = await open(dirname(path), 'r');
|
|
18
|
+
try {
|
|
19
|
+
await directory.sync();
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await directory.close();
|
|
23
|
+
}
|
|
24
|
+
} }
|
|
25
|
+
/** Native SDK file effects have retained input/output bytes and never repeat an uncertain call. */
|
|
26
|
+
export class SdkFiles {
|
|
27
|
+
workspace;
|
|
28
|
+
research;
|
|
29
|
+
backend;
|
|
30
|
+
constructor(workspace, research, backend) {
|
|
31
|
+
this.workspace = workspace;
|
|
32
|
+
this.research = research;
|
|
33
|
+
this.backend = backend;
|
|
34
|
+
}
|
|
35
|
+
async loaders(input) {
|
|
36
|
+
const artifact = await this.workspace.artifacts.capture(input, 'input'), backend = await this.backend();
|
|
37
|
+
if (!backend.sdkFile)
|
|
38
|
+
fail('configuration', 'Native SDK file workflows are unavailable.');
|
|
39
|
+
return { inputSha256: artifact.sha256, ...await backend.sdkFile('loaders', { input: await this.workspace.artifacts.verify(artifact) }) };
|
|
40
|
+
}
|
|
41
|
+
async exporters(selected) {
|
|
42
|
+
return this.research.ordered(selected, async () => {
|
|
43
|
+
const backend = await this.backend();
|
|
44
|
+
if (!backend.sdkFile)
|
|
45
|
+
fail('configuration', 'Native SDK file workflows are unavailable.');
|
|
46
|
+
await backend.open(selected);
|
|
47
|
+
const snapshot = await backend.saved(selected);
|
|
48
|
+
return { ...selected, value: await backend.sdkFile('exporters', { snapshot: await this.workspace.artifacts.verify(snapshot) }) };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async copy(files, directory, recovery = false) {
|
|
52
|
+
try {
|
|
53
|
+
await mkdir(directory, { mode: 0o700 });
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (!recovery || error.code !== 'EEXIST')
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
if (await realpath(directory) !== resolve(directory) || (await lstat(directory)).isSymbolicLink())
|
|
60
|
+
fail('binding', 'Export destination is not the requested ordinary directory.');
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
if (!/^[a-zA-Z0-9_.-]{1,160}$/.test(file.name) || file.name === '.' || file.name === '..')
|
|
63
|
+
fail('binding', 'Exporter produced an unsupported output filename.');
|
|
64
|
+
const source = await this.workspace.artifacts.verify(file.artifact), output = join(directory, file.name);
|
|
65
|
+
try {
|
|
66
|
+
await copyFile(source, output, constants.COPYFILE_EXCL);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (!recovery || error.code !== 'EEXIST')
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
if ((await this.workspace.artifacts.capture(output, 'input')).id !== file.artifact.id)
|
|
73
|
+
fail('partial', 'Exported file bytes differ from retained native output.');
|
|
74
|
+
// Artifact copies retain their read-only mode. The owned user output needs a writable
|
|
75
|
+
// descriptor for FlushFileBuffers on Windows; verify existing recovery bytes first.
|
|
76
|
+
await chmod(output, 0o600);
|
|
77
|
+
const handle = await open(output, 'r+');
|
|
78
|
+
try {
|
|
79
|
+
await handle.sync();
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await handle.close();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (process.platform !== 'win32')
|
|
86
|
+
for (const path of [directory, dirname(directory)]) {
|
|
87
|
+
const handle = await open(path, 'r');
|
|
88
|
+
try {
|
|
89
|
+
await handle.sync();
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await handle.close();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async completedOutput(stage, request, operation) {
|
|
97
|
+
const marker = object(JSON.parse(await readFile(join(stage, 'native-result.json'), 'utf8')));
|
|
98
|
+
if (marker.request !== request || marker.operation !== operation || object(marker.value).state !== 'exported' || !Array.isArray(marker.files) || !marker.files.length || marker.files.length > 64)
|
|
99
|
+
fail('binding', 'Native file completion receipt differs from the retained request.');
|
|
100
|
+
const files = [];
|
|
101
|
+
for (const item of marker.files) {
|
|
102
|
+
const row = object(item), name = text(row.name, 160);
|
|
103
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(name) || name === '.' || name === '..')
|
|
104
|
+
fail('binding', 'Native output name is not a simple owned file.');
|
|
105
|
+
const path = join(stage, 'outputs', name);
|
|
106
|
+
if (!(await lstat(path)).isFile() || (await lstat(path)).isSymbolicLink())
|
|
107
|
+
fail('binding', 'Native output is not a regular owned file.');
|
|
108
|
+
const artifact = await this.workspace.artifacts.capture(path, 'input');
|
|
109
|
+
if (artifact.sha256 !== row.sha256 || artifact.size !== row.size)
|
|
110
|
+
fail('partial', 'Native output changed after its completion receipt.');
|
|
111
|
+
files.push({ name, artifact });
|
|
112
|
+
}
|
|
113
|
+
return { files, value: marker.value };
|
|
114
|
+
}
|
|
115
|
+
async databaseQuery(selected, operation, database, p) {
|
|
116
|
+
const artifact = await this.workspace.artifacts.capture(database, 'input');
|
|
117
|
+
if (artifact.size > 268435456)
|
|
118
|
+
fail('limit', 'Function database exceeds 256 MiB.');
|
|
119
|
+
return this.research.ordered(selected, async () => {
|
|
120
|
+
const backend = await this.backend();
|
|
121
|
+
if (!backend.sdkFile)
|
|
122
|
+
fail('configuration', 'Native function databases are unavailable.');
|
|
123
|
+
await backend.open(selected);
|
|
124
|
+
const snapshot = await backend.saved(selected), value = await backend.sdkFile(operation, { ...p, database: await this.workspace.artifacts.verify(artifact), snapshot: await this.workspace.artifacts.verify(snapshot) });
|
|
125
|
+
return { ...selected, databaseSha256: artifact.sha256, sourceSnapshotSha256: snapshot.sha256, value };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async export(selected, operation, parameters, directory, callerId) {
|
|
129
|
+
const reserved = this.workspace.reserveRequest(selected, 'ghidra.sdk-file.' + operation, { ...parameters, outputDirectory: directory }, callerId);
|
|
130
|
+
return this.research.ordered(selected, async () => {
|
|
131
|
+
const request = this.workspace.request(reserved.request.id), stage = join(this.workspace.directory, 'sdk-exports', request.id), receiptPath = join(stage, 'receipt.json');
|
|
132
|
+
if (reserved.replay && request.state !== 'accepted') {
|
|
133
|
+
if (request.state === 'applied')
|
|
134
|
+
return request.result;
|
|
135
|
+
let saved;
|
|
136
|
+
try {
|
|
137
|
+
saved = object(JSON.parse(await readFile(receiptPath, 'utf8')));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
let completed;
|
|
141
|
+
try {
|
|
142
|
+
completed = await this.completedOutput(stage, request.id, operation);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
fail('unknown', 'Native export has no verified completed receipt; the original call is not repeated.');
|
|
146
|
+
}
|
|
147
|
+
saved = { request: request.id, operation, ...completed };
|
|
148
|
+
await persist(receiptPath, saved);
|
|
149
|
+
}
|
|
150
|
+
if (saved.request !== request.id || saved.operation !== operation || !Array.isArray(saved.files))
|
|
151
|
+
fail('binding', 'Native export receipt differs from the retained intent.');
|
|
152
|
+
const files = saved.files;
|
|
153
|
+
await this.copy(files, directory, true);
|
|
154
|
+
const current = this.workspace.context(selected);
|
|
155
|
+
const result = { ...selected, state: 'exported', outputDirectory: directory, files: files.map(file => ({ name: file.name, sha256: file.artifact.sha256, size: file.artifact.size })), value: saved.value };
|
|
156
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, {}, result);
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
await lstat(directory);
|
|
161
|
+
fail('exists', 'Export directory already exists; choose a new directory.');
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error.code !== 'ENOENT')
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
const backend = await this.backend();
|
|
168
|
+
if (!backend.sdkFile)
|
|
169
|
+
fail('configuration', 'Native SDK file workflows are unavailable.');
|
|
170
|
+
await backend.open(selected);
|
|
171
|
+
const snapshot = await backend.saved(selected), current = this.workspace.context(selected);
|
|
172
|
+
await mkdir(stage, { recursive: true, mode: 0o700 });
|
|
173
|
+
const outputs = join(stage, 'outputs');
|
|
174
|
+
await mkdir(outputs, { recursive: true, mode: 0o700 });
|
|
175
|
+
if (await realpath(outputs) !== resolve(outputs) || (await lstat(outputs)).isSymbolicLink() || (await readdir(outputs)).length)
|
|
176
|
+
fail('binding', 'Accepted file preparation must retain an empty owned output directory.');
|
|
177
|
+
this.workspace.startRequest(request.id, { kind: 'sdk-file', operation, snapshot, generation: current.generation });
|
|
178
|
+
try {
|
|
179
|
+
const filename = operation === 'archive-export' ? 'types.gdt' : operation === 'fid-build' ? 'database.fidb' : operation === 'bsim-build' ? 'database.mv.db' : 'program.out';
|
|
180
|
+
const value = await backend.sdkFile(operation, { ...parameters, request_id: request.id, receipt: join(stage, 'native-result.json'), snapshot: await this.workspace.artifacts.verify(snapshot), output: join(outputs, filename) });
|
|
181
|
+
if (value.state !== 'exported')
|
|
182
|
+
fail('partial', 'Native exporter did not establish a completed output.');
|
|
183
|
+
const completed = await this.completedOutput(stage, request.id, operation), files = completed.files;
|
|
184
|
+
if (stable(completed.value) !== stable(value))
|
|
185
|
+
fail('partial', 'Native file reply differs from its durable receipt.');
|
|
186
|
+
await persist(receiptPath, { request: request.id, operation, files, value });
|
|
187
|
+
await this.copy(files, directory);
|
|
188
|
+
const result = { ...selected, state: 'exported', outputDirectory: directory, files: files.map(file => ({ name: file.name, sha256: file.artifact.sha256, size: file.artifact.size })), value };
|
|
189
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, { workingSnapshot: snapshot }, result);
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
this.workspace.recordRequest(request.id, 'unknown', { ...selected, state: 'unknown' });
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function json(value) { return parseDomainJson(text(value, 16000)); }
|
|
200
|
+
export function sdkFileCommands(service) {
|
|
201
|
+
const outputs = [...selectedOptions, { flags: '--output-directory <directory>', description: 'New output directory including all exporter companion files', required: true },
|
|
202
|
+
{ flags: '--request-id <id>', description: 'Retrieve retained export bytes without repeating native export' }];
|
|
203
|
+
const databases = [];
|
|
204
|
+
for (const kind of ['fid', 'bsim']) {
|
|
205
|
+
databases.push(command('build-' + kind, 'Build a genuine native ' + kind + ' database from the saved private snapshot', ({ options: o }, c) => service(c).export(scope(o), kind + '-build', { library: text(o.library, 512), ...(kind === 'fid' ? { version: text(o.libraryVersion, 128) } : {}) }, resolve(c.cwd, text(o.outputDirectory, 4096)), o.requestId === undefined ? undefined : id(o.requestId)), { permission: Permission.Update, options: [...outputs, { flags: '--library <name>', description: 'Native library/database name', required: true }, ...(kind === 'fid' ? [{ flags: '--library-version <version>', description: 'Native library version', required: true }] : [])] }));
|
|
206
|
+
databases.push(command(kind + '-query', 'Query one explicitly consumed native ' + kind + ' database with provenance and no-match semantics', ({ options: o }, c) => {
|
|
207
|
+
const p = { limit: number(o.limit, 1000, 1) };
|
|
208
|
+
if (o.function !== undefined)
|
|
209
|
+
p.function = address(o.function);
|
|
210
|
+
if (kind === 'fid') {
|
|
211
|
+
const threshold = Number(o.threshold);
|
|
212
|
+
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 100000)
|
|
213
|
+
fail('invalid', 'FID threshold must be 0–100000 native code units.');
|
|
214
|
+
p.threshold = threshold;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const similarity = Number(o.similarity), significance = Number(o.significance);
|
|
218
|
+
if (!Number.isFinite(similarity) || similarity < 0 || similarity > 1 || !Number.isFinite(significance) || significance < 0 || significance > 1000000)
|
|
219
|
+
fail('invalid', 'Invalid native BSim similarity/significance threshold.');
|
|
220
|
+
p.similarity = similarity;
|
|
221
|
+
p.significance = significance;
|
|
222
|
+
}
|
|
223
|
+
return service(c).databaseQuery(scope(o), kind + '-query', resolve(c.cwd, text(o.database, 4096)), p);
|
|
224
|
+
}, { permission: Permission.ReadOnly, options: [...selectedOptions, { flags: '--database <file>', description: 'Native FID or BSim/H2 file; immutable bytes are consumed first', required: true },
|
|
225
|
+
{ flags: '--function <address>', description: 'Exact function entry', ...(kind === 'bsim' ? { required: true } : {}) }, { flags: '--limit <count>', description: 'Maximum returned matches', defaultValue: 20 },
|
|
226
|
+
...(kind === 'fid' ? [{ flags: '--threshold <score>', description: 'Native matching code-unit threshold', defaultValue: 14.6 }] : [{ flags: '--similarity <number>', description: 'Native feature similarity 0–1', defaultValue: 0.7 }, { flags: '--significance <number>', description: 'Native feature significance', defaultValue: 0 }])] }));
|
|
227
|
+
}
|
|
228
|
+
return [command('libraries', 'Native FID and BSim/H2 library identity/similarity databases', databases), command('formats', 'Native loaders and exporters', [
|
|
229
|
+
command('loaders', 'Read installed loader names and supported specs/options for consumed bytes', ({ options: o }, c) => service(c).loaders(resolve(c.cwd, text(o.input, 4096))), { permission: Permission.ReadOnly, options: [{ flags: '--input <file>', description: 'Input file to inspect', required: true }] }),
|
|
230
|
+
command('exporters', 'Read installed exporters and their actual options for this program', ({ options: o }, c) => service(c).exporters(scope(o)), { permission: Permission.ReadOnly, options: selectedOptions }),
|
|
231
|
+
command('export', 'Export through an explicitly selected native exporter with typed options/ranges', ({ options: o }, c) => {
|
|
232
|
+
const p = { exporter: text(o.exporter, 512), options: object(json(o.options)) };
|
|
233
|
+
if (o.ranges !== undefined) {
|
|
234
|
+
const ranges = json(o.ranges);
|
|
235
|
+
if (!Array.isArray(ranges) || !ranges.length || ranges.length > 64)
|
|
236
|
+
fail('invalid', 'Export ranges require 1–64 start/end objects.');
|
|
237
|
+
p.ranges = ranges;
|
|
238
|
+
}
|
|
239
|
+
return service(c).export(scope(o), 'export', p, resolve(c.cwd, text(o.outputDirectory, 4096)), o.requestId === undefined ? undefined : id(o.requestId));
|
|
240
|
+
}, { permission: Permission.Update, options: [...outputs, { flags: '--exporter <id>', description: 'Explicit native exporter name/ID', required: true }, { flags: '--options <json>', description: 'Named primitive native options', defaultValue: '{}' }, { flags: '--ranges <json>', description: 'Address ranges with start/end; exporter must support restricted output' }] }),
|
|
241
|
+
]), command('types', 'Native type archives', [
|
|
242
|
+
command('archive-export', 'Export 1–256 explicit type paths to a native GDT with checked layout', ({ options: o }, c) => {
|
|
243
|
+
const types = json(o.types);
|
|
244
|
+
if (!Array.isArray(types) || !types.length || types.length > 256 || types.some(v => typeof v !== 'string' || !v.startsWith('/')))
|
|
245
|
+
fail('invalid', 'Types must be 1–256 absolute type paths.');
|
|
246
|
+
return service(c).export(scope(o), 'archive-export', { types }, resolve(c.cwd, text(o.outputDirectory, 4096)), o.requestId === undefined ? undefined : id(o.requestId));
|
|
247
|
+
}, { permission: Permission.Update, options: [...outputs, { flags: '--types <json>', description: 'Selected native type paths', required: true }] }),
|
|
248
|
+
])];
|
|
249
|
+
}
|