@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,808 @@
|
|
|
1
|
+
import { fail, id, object, text, digest } from './model.js';
|
|
2
|
+
import { fieldKey, scalarMerge, scalarRefresh } from './merge.js';
|
|
3
|
+
import { verifyMutation } from './mutation-effects.js';
|
|
4
|
+
import { transferResult } from './cross-results.js';
|
|
5
|
+
import { CliError } from '@eyeauras/cli-factory';
|
|
6
|
+
export function address(value) {
|
|
7
|
+
const input = text(value, 128);
|
|
8
|
+
const match = /^(?:([A-Za-z0-9_]+):)?(?:0x)?([a-fA-F0-9]+)$/.exec(input);
|
|
9
|
+
if (!match)
|
|
10
|
+
fail('invalid', 'Address must be hexadecimal with an optional address-space prefix.');
|
|
11
|
+
return (match[1] ? match[1] + ':' : '') + match[2].toLowerCase().replace(/^0+(?=.)/, '');
|
|
12
|
+
}
|
|
13
|
+
export const publicAddress = (value) => { const normalized = address(value); return normalized.includes(':') ? normalized : '0x' + normalized; };
|
|
14
|
+
export function scalarText(value) {
|
|
15
|
+
if (value === null || value === '')
|
|
16
|
+
return null;
|
|
17
|
+
if (typeof value !== 'string' || value.length > 16000 || value.includes('\0'))
|
|
18
|
+
fail('invalid', 'Comment must be a string of at most 16000 characters, or null.');
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export function commentKind(value) {
|
|
22
|
+
const kind = text(value);
|
|
23
|
+
if (!['eol', 'pre', 'post', 'plate', 'repeatable'].includes(kind))
|
|
24
|
+
fail('invalid', 'Comment kind must be eol, pre, post, plate or repeatable.');
|
|
25
|
+
return kind;
|
|
26
|
+
}
|
|
27
|
+
export function number(value, maximum, minimum = 0) {
|
|
28
|
+
if (typeof value !== 'number' && (typeof value !== 'string' || !/^\d+$/.test(value)))
|
|
29
|
+
fail('invalid', 'Numeric argument must be an integer.');
|
|
30
|
+
const result = typeof value === 'number' ? value : Number(value);
|
|
31
|
+
if (!Number.isSafeInteger(result) || result < minimum || result > maximum)
|
|
32
|
+
fail('invalid', 'Numeric argument is outside its declared range.');
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
export function scope(options) { return { session: id(options.session), target: id(options.target) }; }
|
|
36
|
+
const count = (context) => context.scalars.length + context.complexChanges.length;
|
|
37
|
+
const revision = (value) => value === undefined ? undefined : 'R' + value;
|
|
38
|
+
function comments(value) {
|
|
39
|
+
const result = object(value), observed = {};
|
|
40
|
+
for (const kind of ['eol', 'pre', 'post', 'plate', 'repeatable']) {
|
|
41
|
+
if (!Object.hasOwn(result, kind))
|
|
42
|
+
fail('partial', 'Comment result omitted a requested field.');
|
|
43
|
+
observed[kind] = scalarText(result[kind]);
|
|
44
|
+
}
|
|
45
|
+
return observed;
|
|
46
|
+
}
|
|
47
|
+
function functionResult(value, selector) {
|
|
48
|
+
const result = object(value), functions = object(result.functions), found = object(functions[selector]);
|
|
49
|
+
if (Object.keys(functions).length !== 1 || found.error || typeof found.name !== 'string' || !found.name || !found.entry_point)
|
|
50
|
+
fail('partial', 'Function result omitted, failed or changed the requested selector.');
|
|
51
|
+
return found;
|
|
52
|
+
}
|
|
53
|
+
function pageResult(value, key, limit, offset) {
|
|
54
|
+
const result = object(value);
|
|
55
|
+
if (!Array.isArray(result[key]) || result[key].length > limit || !Number.isSafeInteger(result.total) || Number(result.total) < 0 ||
|
|
56
|
+
result.offset !== offset || result.limit !== limit || result.count !== undefined && result.count !== result[key].length)
|
|
57
|
+
fail('partial', 'Paged result has inconsistent items, counts or selectors.');
|
|
58
|
+
return { result, items: result[key], total: Number(result.total) };
|
|
59
|
+
}
|
|
60
|
+
/** One ordered working context per tuple, shared by every transport through the Factory owner. */
|
|
61
|
+
export class Research {
|
|
62
|
+
workspace;
|
|
63
|
+
backend;
|
|
64
|
+
tasks;
|
|
65
|
+
constructor(workspace, backend, tasks = new Map()) {
|
|
66
|
+
this.workspace = workspace;
|
|
67
|
+
this.backend = backend;
|
|
68
|
+
this.tasks = tasks;
|
|
69
|
+
}
|
|
70
|
+
async ordered(selected, action) {
|
|
71
|
+
const key = this.workspace.context(selected).id, previous = this.tasks.get(key);
|
|
72
|
+
const task = (previous ? previous.catch(() => { }) : Promise.resolve()).then(action);
|
|
73
|
+
this.tasks.set(key, task);
|
|
74
|
+
try {
|
|
75
|
+
return await task;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
if (this.tasks.get(key) === task)
|
|
79
|
+
this.tasks.delete(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async boundary(selected, action) {
|
|
83
|
+
const key = 'command:' + this.workspace.context(selected).id, previous = this.tasks.get(key), task = (previous ? previous.catch(() => { }) : Promise.resolve()).then(action);
|
|
84
|
+
this.tasks.set(key, task);
|
|
85
|
+
try {
|
|
86
|
+
return await task;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
if (this.tasks.get(key) === task)
|
|
90
|
+
this.tasks.delete(key);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
status(selected) {
|
|
94
|
+
const context = this.workspace.context(selected), model = this.workspace.model(selected.target), unresolved = this.workspace.unresolved(selected);
|
|
95
|
+
const external = this.workspace.unresolvedSessionAdministration(selected.session, selected.target);
|
|
96
|
+
return { ...selected, externalRequests: external.map(request => ({ ...(request.callerId ? { requestId: request.callerId } : {}), operation: request.operation, state: request.state })), externalRequestsTruncated: external.length === 100,
|
|
97
|
+
ghidra: { syncPolicy: context.syncPolicy ?? { publish: 'manual', refresh: 'manual' }, state: unresolved.length ? 'needs_recovery' : context.state, baseRevision: revision(context.baseRevision),
|
|
98
|
+
sharedRevision: revision(model.sharedRevision), pendingChanges: count(context),
|
|
99
|
+
mergeState: model.state === 'unknown' ? 'unknown' : 'idle',
|
|
100
|
+
conflicts: context.scalars.filter(change => change.conflict).map(change => ({ address: publicAddress(change.address), field: change.field,
|
|
101
|
+
original: change.original, local: change.local, shared: change.conflict.shared })),
|
|
102
|
+
unresolvedRequests: unresolved.map(request => ({ ...(request.callerId ? { requestId: request.callerId } : {}), operation: request.operation, state: request.state })) } };
|
|
103
|
+
}
|
|
104
|
+
async open(target, session, settings, callerId, mode) {
|
|
105
|
+
// Mode belongs to the request's effective intent. It does not alter target load identity.
|
|
106
|
+
const reserved = this.workspace.reserveOpen(target, session, settings, callerId, mode);
|
|
107
|
+
// Older reconciliation accidentally persisted internal context fields in ready results.
|
|
108
|
+
// Project the public envelope without rewriting an immutable historical request record.
|
|
109
|
+
const opened = (value) => { const result = object(value); return Object.fromEntries(['session', 'target', 'state', 'baseRevision', 'sharedRevision', 'pendingChanges'].filter(key => Object.hasOwn(result, key)).map(key => [key, result[key]])); };
|
|
110
|
+
const retained = (state) => ({ session: reserved.context.session, target, state, ...(callerId ? { requestId: callerId } : {}) });
|
|
111
|
+
const next = [['sessions', 'status', reserved.context.session, '--target', target], ['ghidra', 'open', '--session', reserved.context.session, '--target', target]];
|
|
112
|
+
const openingFailure = (error, state) => new CliError(error instanceof CliError ? error.message : 'Ghidra open did not complete; the context is retained for reconciliation.', {
|
|
113
|
+
code: error instanceof CliError ? error.code : 'coordinator.unknown', result: { ...retained(state), ...(error instanceof CliError && error.result && typeof error.result === 'object' ? { detail: error.result } : {}) }, next
|
|
114
|
+
});
|
|
115
|
+
return this.ordered(reserved.context, async () => {
|
|
116
|
+
if (this.workspace.request(reserved.request.id).state === 'applied')
|
|
117
|
+
return opened(this.workspace.request(reserved.request.id).result);
|
|
118
|
+
let backend;
|
|
119
|
+
try {
|
|
120
|
+
backend = await this.backend();
|
|
121
|
+
await this.sharedOrder(target, () => this.reconcile({ session: reserved.context.session, target }, backend));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
throw openingFailure(error, this.workspace.request(reserved.request.id).state);
|
|
125
|
+
}
|
|
126
|
+
const request = this.workspace.request(reserved.request.id);
|
|
127
|
+
if (request.state === 'applied')
|
|
128
|
+
return opened(request.result);
|
|
129
|
+
if (reserved.replay && request.state !== 'accepted')
|
|
130
|
+
throw new CliError('This open retains an unproved outcome; inspect the retained context before continuing.', { code: 'coordinator.unknown', result: retained(request.state), next });
|
|
131
|
+
if (this.workspace.unresolved(reserved.context).some(other => other.id !== request.id)) {
|
|
132
|
+
const result = { session: reserved.context.session, target, state: 'blocked' };
|
|
133
|
+
this.workspace.recordRequest(request.id, 'failed', result);
|
|
134
|
+
fail('blocked', 'This context retains an unproved effect; continuation did not start a second operation.', result);
|
|
135
|
+
}
|
|
136
|
+
this.workspace.startRequest(request.id);
|
|
137
|
+
try {
|
|
138
|
+
await backend.open(reserved.context, request.id);
|
|
139
|
+
if (mode)
|
|
140
|
+
await backend.mode(reserved.context, mode);
|
|
141
|
+
const context = this.workspace.context(reserved.context);
|
|
142
|
+
const result = { session: context.session, target: context.target, state: 'ready', baseRevision: revision(context.baseRevision),
|
|
143
|
+
sharedRevision: revision(this.workspace.model(target).sharedRevision), pendingChanges: count(context) };
|
|
144
|
+
this.workspace.completeSavedRequest(context, request.id, context.generation, {}, result);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const result = retained('unknown');
|
|
149
|
+
this.workspace.recordRequest(request.id, 'unknown', result);
|
|
150
|
+
throw openingFailure(error, 'unknown');
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async read(selected, operation, parameters) {
|
|
155
|
+
return this.ordered(selected, async () => {
|
|
156
|
+
const backend = await this.backend();
|
|
157
|
+
await backend.open(selected);
|
|
158
|
+
return backend.call(selected, operation, parameters);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async scripts(selected, filter, limit, offset) {
|
|
162
|
+
return this.ordered(selected, async () => {
|
|
163
|
+
const backend = await this.backend();
|
|
164
|
+
await backend.open(selected);
|
|
165
|
+
if (!backend.scripts)
|
|
166
|
+
fail('configuration', 'Selected backend does not provide the configured script inventory.');
|
|
167
|
+
return backend.scripts(selected, filter, limit, offset);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async paired(selected, other, action) {
|
|
171
|
+
const scopes = [selected, other].sort((a, b) => this.workspace.context(a).id.localeCompare(this.workspace.context(b).id));
|
|
172
|
+
if (this.workspace.context(selected).id === this.workspace.context(other).id)
|
|
173
|
+
return this.ordered(selected, action);
|
|
174
|
+
return this.ordered(scopes[0], () => this.ordered(scopes[1], action));
|
|
175
|
+
}
|
|
176
|
+
async crossRead(selected, other, operation, parameters) {
|
|
177
|
+
return this.paired(selected, other, async () => {
|
|
178
|
+
const backend = await this.backend();
|
|
179
|
+
await backend.open(selected);
|
|
180
|
+
await backend.open(other);
|
|
181
|
+
if (!backend.pair)
|
|
182
|
+
fail('configuration', 'Selected backend does not support explicit cross-target comparisons.');
|
|
183
|
+
return backend.pair(selected, other, await backend.saved(other), operation, parameters);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async savedScalars(context, backend, snapshot) {
|
|
187
|
+
if (!context.scalars.length)
|
|
188
|
+
return context.scalars;
|
|
189
|
+
if (!backend.fields)
|
|
190
|
+
fail('configuration', 'Tracked scalar fields require saved-state inspection.');
|
|
191
|
+
const fields = await backend.fields(snapshot, context.scalars);
|
|
192
|
+
return context.scalars.flatMap(change => {
|
|
193
|
+
const observed = fields[fieldKey(change)];
|
|
194
|
+
if (!observed)
|
|
195
|
+
fail('partial', 'Saved inspection omitted a tracked scalar field.');
|
|
196
|
+
if (!observed.exists)
|
|
197
|
+
return [{ ...change, local: null, removed: true }];
|
|
198
|
+
const { removed, ...previous } = change;
|
|
199
|
+
if (observed.value === previous.original && !previous.conflict)
|
|
200
|
+
return [];
|
|
201
|
+
return [{ ...previous, local: observed.value }];
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async transfer(selected, source, dryRun, callerId) {
|
|
205
|
+
if (this.workspace.context(selected).id === this.workspace.context(source).id)
|
|
206
|
+
fail('invalid', 'Documentation source and destination contexts must differ.');
|
|
207
|
+
const reserved = this.workspace.reserveRequest(selected, '/merge_program_documentation', { source, dryRun }, callerId);
|
|
208
|
+
return this.paired(selected, source, async () => {
|
|
209
|
+
const request = this.workspace.request(reserved.request.id);
|
|
210
|
+
if (reserved.replay && request.state !== 'accepted') {
|
|
211
|
+
if (request.state === 'applied')
|
|
212
|
+
return request.result;
|
|
213
|
+
fail('unknown', 'This documentation transfer retains an unproved outcome and is not repeated.', request.result);
|
|
214
|
+
}
|
|
215
|
+
const backend = await this.backend();
|
|
216
|
+
await backend.open(source);
|
|
217
|
+
const sourceSnapshot = await backend.saved(source);
|
|
218
|
+
await backend.open(selected);
|
|
219
|
+
if (!backend.transfer || !backend.transferCheck)
|
|
220
|
+
fail('configuration', 'Selected backend cannot verify documentation transfer.');
|
|
221
|
+
const context = this.workspace.context(selected);
|
|
222
|
+
this.workspace.startRequest(request.id, { kind: 'transfer', generation: context.generation, workingSnapshot: context.workingSnapshot, source, sourceSnapshot, dryRun });
|
|
223
|
+
try {
|
|
224
|
+
const value = transferResult(await backend.transfer(selected, sourceSnapshot, dryRun), dryRun);
|
|
225
|
+
const snapshot = await backend.saved(selected);
|
|
226
|
+
if (!dryRun && transferResult(await backend.transferCheck(selected, sourceSnapshot, snapshot), true).total !== 0)
|
|
227
|
+
fail('partial', 'Saved destination retains untransferred documentation.');
|
|
228
|
+
const scalars = dryRun ? context.scalars : await this.savedScalars(context, backend, snapshot);
|
|
229
|
+
const complexChanges = dryRun || value.total === 0 ? context.complexChanges : [...context.complexChanges, request.id];
|
|
230
|
+
const result = { ...selected, source, state: dryRun ? 'preview' : 'applied', sharedState: 'pending',
|
|
231
|
+
pendingChanges: scalars.length + complexChanges.length, value };
|
|
232
|
+
this.workspace.completeSavedRequest(selected, request.id, context.generation, { workingSnapshot: snapshot, complexChanges, scalars }, result);
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
let savedSnapshot;
|
|
237
|
+
try {
|
|
238
|
+
savedSnapshot = await backend.saved(selected);
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
this.workspace.recordRequest(request.id, 'unknown', { ...selected, source, state: 'unknown', sourceSnapshot, ...(savedSnapshot ? { savedSnapshot } : {}) });
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
async strings(selected, contains, limit, offset) {
|
|
247
|
+
const pattern = text(contains, 4096).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
248
|
+
const value = await this.read(selected, '/search_strings', { search_term: pattern, min_length: 0, offset, limit });
|
|
249
|
+
const page = pageResult(value, 'matches', limit, offset);
|
|
250
|
+
const items = page.items.map(item => {
|
|
251
|
+
const row = object(item);
|
|
252
|
+
if (typeof row.value !== 'string')
|
|
253
|
+
fail('malformed', 'String result lacks decoded text.');
|
|
254
|
+
return { address: publicAddress(row.address), text: row.value };
|
|
255
|
+
});
|
|
256
|
+
return { ...selected, items, offset, limit, total: page.total, truncated: object(value).truncated === true || offset + items.length < page.total,
|
|
257
|
+
...(object(value).scan ? { scan: object(value).scan, totalKnown: object(value).total_known !== false } : {}) };
|
|
258
|
+
}
|
|
259
|
+
async function(selected, at, decompile = false) {
|
|
260
|
+
return this.ordered(selected, async () => {
|
|
261
|
+
const backend = await this.backend();
|
|
262
|
+
await backend.open(selected);
|
|
263
|
+
const value = await backend.call(selected, '/get_functions', { functions: at, fields: decompile ? 'entry_point,signature,decompiled_code' : 'entry_point,signature', include_call_context: false });
|
|
264
|
+
const result = functionResult(value, at);
|
|
265
|
+
if (typeof result.signature !== 'string')
|
|
266
|
+
fail('partial', 'Function result lacks its signature.');
|
|
267
|
+
if (decompile && typeof result.decompiled_code !== 'string')
|
|
268
|
+
fail('partial', 'Function decompilation did not produce text.');
|
|
269
|
+
const listing = object(await backend.call(selected, '/get_comment', { address: result.entry_point }));
|
|
270
|
+
return { ...selected, address: publicAddress(result.entry_point), name: result.name, signature: result.signature, comments: comments(listing),
|
|
271
|
+
...(decompile ? { code: result.decompiled_code, truncated: Boolean(object(result.truncation ?? {}).decompiled_code) } : {}) };
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
async xrefs(selected, at, limit, offset) {
|
|
275
|
+
return this.ordered(selected, async () => {
|
|
276
|
+
const backend = await this.backend();
|
|
277
|
+
await backend.open(selected);
|
|
278
|
+
const page = pageResult(await backend.call(selected, '/get_xrefs_to', { address: at, limit, offset }), 'references', limit, offset);
|
|
279
|
+
const items = [];
|
|
280
|
+
for (const item of page.items) {
|
|
281
|
+
const row = object(item);
|
|
282
|
+
if (typeof row.type !== 'string' || !row.type)
|
|
283
|
+
fail('partial', 'Reference result lacks its kind.');
|
|
284
|
+
// Ghidra's external-entry reference is metadata, not a navigable address.
|
|
285
|
+
if (row.from_address === 'Entry Point' && row.type === 'EXTERNAL') {
|
|
286
|
+
items.push({ from: null, to: publicAddress(at), kind: row.type, source: 'entry-point' });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const from = address(row.from_address);
|
|
290
|
+
let containing;
|
|
291
|
+
if (row.from_function !== undefined) {
|
|
292
|
+
const found = functionResult(await backend.call(selected, '/get_functions', { functions: from, fields: 'entry_point', include_call_context: false }), from);
|
|
293
|
+
if (found.name !== row.from_function)
|
|
294
|
+
fail('binding', 'Reference function changed during ordered inspection.');
|
|
295
|
+
containing = { address: publicAddress(found.entry_point), name: found.name };
|
|
296
|
+
}
|
|
297
|
+
items.push({ from: publicAddress(from), to: publicAddress(at), kind: row.type, ...(containing ? { function: containing } : {}) });
|
|
298
|
+
}
|
|
299
|
+
return { ...selected, items, offset, limit, total: page.total, truncated: offset + items.length < page.total };
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
async comment(selected, at, kind) {
|
|
303
|
+
const result = object(await this.read(selected, '/get_comment', { address: at }));
|
|
304
|
+
if (address(result.address) !== address(at))
|
|
305
|
+
fail('partial', 'Comment response changed its address selector.');
|
|
306
|
+
const values = comments(result), context = this.workspace.context(selected);
|
|
307
|
+
const change = context.scalars.find(change => change.address === address(at) && change.field === 'comment.' + kind);
|
|
308
|
+
return { ...selected, address: publicAddress(at), ...(kind ? { kind, text: values[kind], sharedState: change?.conflict ? 'conflict' : change ? 'pending' : 'shared' } : { comments: values }) };
|
|
309
|
+
}
|
|
310
|
+
async edit(selected, at, field, value, callerId, resolution) {
|
|
311
|
+
at = address(at);
|
|
312
|
+
const reserved = this.workspace.reserveRequest(selected, field === 'function.name' ? 'ghidra.functions.rename' : 'ghidra.comments.set', { address: at, field, value, ...(resolution ? { resolution } : {}) }, callerId);
|
|
313
|
+
const action = async () => {
|
|
314
|
+
const request = this.workspace.request(reserved.request.id);
|
|
315
|
+
if (reserved.replay) {
|
|
316
|
+
if (request.state === 'applied')
|
|
317
|
+
return request.result;
|
|
318
|
+
if (request.state !== 'accepted')
|
|
319
|
+
return { ...selected, state: request.state, ...(callerId ? { requestId: callerId } : {}) };
|
|
320
|
+
}
|
|
321
|
+
const backend = await this.backend();
|
|
322
|
+
// Restore before starting this mutation; other uncertain effects still block the context.
|
|
323
|
+
await backend.open(selected);
|
|
324
|
+
const before = this.workspace.context(selected);
|
|
325
|
+
const previous = before.scalars.find(change => change.address === at && change.field === field);
|
|
326
|
+
let observed;
|
|
327
|
+
if (field === 'function.name') {
|
|
328
|
+
observed = String(functionResult(await backend.call(selected, '/get_functions', { functions: at, fields: 'entry_point', include_call_context: false }), at).name);
|
|
329
|
+
}
|
|
330
|
+
else
|
|
331
|
+
observed = comments(await backend.call(selected, '/get_comment', { address: at }))[field.slice(8)];
|
|
332
|
+
let original = previous ? previous.original : observed, dependency = previous?.dependency;
|
|
333
|
+
if (resolution) {
|
|
334
|
+
if (!previous?.conflict)
|
|
335
|
+
fail('conflict', 'Explicit resolution requires an existing field conflict.');
|
|
336
|
+
if (!backend.latest || !backend.fields)
|
|
337
|
+
fail('configuration', 'Resolution requires verified shared-state inspection.');
|
|
338
|
+
if (this.workspace.activePublication(selected.target) || this.workspace.model(selected.target).state === 'unknown')
|
|
339
|
+
fail('blocked', 'Shared publication outcome must be reconciled before resolving a field.');
|
|
340
|
+
const shared = await backend.latest(selected.target);
|
|
341
|
+
this.workspace.observeShared(selected.target, shared.revision, shared.snapshot);
|
|
342
|
+
const current = (await backend.fields(shared.snapshot, [previous]))[fieldKey(previous)];
|
|
343
|
+
if (!current || !current.exists)
|
|
344
|
+
fail('conflict', 'The shared field dependency disappeared; transfer work onto a current context.');
|
|
345
|
+
if (current.value !== resolution.expectedShared) {
|
|
346
|
+
const result = { ...selected, state: 'conflict', reason: 'shared_changed', expected: resolution.expectedShared, shared: current.value, local: observed, applied: 0 };
|
|
347
|
+
this.workspace.recordRequest(request.id, 'failed', result);
|
|
348
|
+
fail('conflict', 'Shared value changed since the explicit decision; local work is retained.', result);
|
|
349
|
+
}
|
|
350
|
+
original = current.value;
|
|
351
|
+
dependency = current.dependency;
|
|
352
|
+
}
|
|
353
|
+
else if (previous?.conflict)
|
|
354
|
+
fail('conflict', 'This field has an unresolved shared conflict; use an explicit resolution.');
|
|
355
|
+
if (dependency === undefined && backend.fields && before.workingSnapshot) {
|
|
356
|
+
const anchor = (await backend.fields(before.workingSnapshot, [{ address: at, field }]))[at + '@' + field];
|
|
357
|
+
if (!anchor)
|
|
358
|
+
fail('partial', 'Saved private dependency inspection omitted the edited field.');
|
|
359
|
+
dependency = anchor.dependency;
|
|
360
|
+
}
|
|
361
|
+
const scalars = before.scalars.filter(change => change.address !== at || change.field !== field);
|
|
362
|
+
if (value !== original)
|
|
363
|
+
scalars.push({ address: at, field, original, local: value, ...(dependency ? { dependency } : {}) });
|
|
364
|
+
const complexChanges = [...before.complexChanges];
|
|
365
|
+
if (previous && previous.local !== observed && !complexChanges.includes(request.id))
|
|
366
|
+
complexChanges.push(request.id);
|
|
367
|
+
const result = { ...selected, state: 'applied', sharedState: scalars.length || complexChanges.length ? 'pending' : 'shared',
|
|
368
|
+
...(resolution ? { conflict: 'resolved' } : {}), pendingChanges: scalars.length + complexChanges.length };
|
|
369
|
+
this.workspace.startRequest(request.id, { kind: 'scalar', address: at, field, value, observed, generation: before.generation,
|
|
370
|
+
workingSnapshot: before.workingSnapshot, scalars, complexChanges, result });
|
|
371
|
+
try {
|
|
372
|
+
const changed = object(await backend.call(selected, field === 'function.name' ? '/rename_function' : '/set_comment', field === 'function.name' ? { old_name: at, new_name: value, strict_mode: 'off' } : { address: at, type: field.slice(8), comment: value ?? '' }));
|
|
373
|
+
if (changed.status !== 'success' && changed.success !== true)
|
|
374
|
+
fail('partial', 'Editing operation was not acknowledged.');
|
|
375
|
+
const after = field === 'function.name' ? String(functionResult(await backend.call(selected, '/get_functions', { functions: at, fields: 'entry_point', include_call_context: false }), at).name) :
|
|
376
|
+
comments(await backend.call(selected, '/get_comment', { address: at }))[field.slice(8)];
|
|
377
|
+
if (after !== value)
|
|
378
|
+
fail('partial', 'Editing readback does not match the requested value.');
|
|
379
|
+
const snapshot = await backend.saved(selected);
|
|
380
|
+
this.workspace.completeSavedRequest(selected, request.id, before.generation, { workingSnapshot: snapshot, scalars, complexChanges }, result);
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
this.workspace.recordRequest(request.id, 'unknown', { ...selected, state: 'unknown' });
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
return this.ordered(selected, () => resolution ? this.sharedOrder(selected.target, action) : action());
|
|
389
|
+
}
|
|
390
|
+
async sharedOrder(target, action) {
|
|
391
|
+
const key = 'shared:' + target, previous = this.tasks.get(key), task = (previous ? previous.catch(() => { }) : Promise.resolve()).then(action);
|
|
392
|
+
this.tasks.set(key, task);
|
|
393
|
+
try {
|
|
394
|
+
return await task;
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
if (this.tasks.get(key) === task)
|
|
398
|
+
this.tasks.delete(key);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async reconcile(selected, backend) {
|
|
402
|
+
const active = this.workspace.activePublication(selected.target);
|
|
403
|
+
if (active) {
|
|
404
|
+
if (active.state === 'preparing')
|
|
405
|
+
this.workspace.rejectPublication(active.id, { session: active.session, target: active.target, state: 'not_published', applied: 0 });
|
|
406
|
+
else if (backend.recover) {
|
|
407
|
+
const observed = await backend.recover(active);
|
|
408
|
+
if (observed.state === 'verified')
|
|
409
|
+
this.workspace.finishPublication(active.id, observed.revision, { session: active.session, target: active.target,
|
|
410
|
+
state: 'merged', sharedRevision: revision(observed.revision), applied: active.scalars.length + (active.complexChanges?.length ?? Number(active.complex)), pendingChanges: 0 });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
for (const request of this.workspace.unresolved(selected)) {
|
|
414
|
+
if (request.operation === 'ghidra.merge') {
|
|
415
|
+
let publication;
|
|
416
|
+
try {
|
|
417
|
+
publication = this.workspace.publication(request.id);
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
if (!String(error.code).endsWith('.notFound'))
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
if (!publication)
|
|
424
|
+
this.workspace.recordRequest(request.id, 'failed', { ...selected, state: 'not_published', applied: 0 });
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (request.operation === 'ghidra.refresh') {
|
|
428
|
+
// A refresh stages a detached candidate; context/result commit together, before install.
|
|
429
|
+
this.workspace.recordRequest(request.id, 'failed', { ...selected, state: 'not_updated', receivedChanges: 0 });
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (request.operation === 'ghidra.open') {
|
|
433
|
+
if (backend.recoverModel && !await backend.recoverModel(selected.target))
|
|
434
|
+
continue;
|
|
435
|
+
try {
|
|
436
|
+
await backend.open(selected, request.id);
|
|
437
|
+
const intent = object(request.intent);
|
|
438
|
+
if (intent.mode)
|
|
439
|
+
await backend.mode(selected, intent.mode);
|
|
440
|
+
const context = this.workspace.context(selected), result = { ...selected, state: 'ready', baseRevision: revision(context.baseRevision),
|
|
441
|
+
sharedRevision: revision(this.workspace.model(selected.target).sharedRevision), pendingChanges: count(context) };
|
|
442
|
+
this.workspace.completeSavedRequest(selected, request.id, context.generation, {}, result);
|
|
443
|
+
}
|
|
444
|
+
catch { /* Saved/native evidence remains unresolved; no new intent is executed. */ }
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
const execution = request.execution === undefined ? undefined : object(request.execution);
|
|
448
|
+
if (execution?.kind === 'discard') {
|
|
449
|
+
try {
|
|
450
|
+
const current = this.workspace.context(selected), base = execution.baseSnapshot, discarded = execution.discardedSnapshot;
|
|
451
|
+
if (!backend.closed || !await backend.closed(selected) || current.generation !== execution.generation || current.baseSnapshot?.id !== base.id || current.workingSnapshot?.id !== discarded.id)
|
|
452
|
+
continue;
|
|
453
|
+
await this.workspace.artifacts.verify(base);
|
|
454
|
+
await this.workspace.artifacts.verify(discarded);
|
|
455
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, { workingSnapshot: base, state: 'stopped', viewEpoch: (current.viewEpoch ?? 0) + 1, scalars: [], complexChanges: [] }, execution.result);
|
|
456
|
+
}
|
|
457
|
+
catch { /* A local switch is reconstructed only from its unchanged retained snapshots. */ }
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (execution?.kind === 'debugger') {
|
|
461
|
+
try {
|
|
462
|
+
if (backend.closed && await backend.closed(selected)) {
|
|
463
|
+
const current = this.workspace.context(selected), outcome = object(request.result ?? {}), retained = outcome.savedDebugger;
|
|
464
|
+
const saved = retained && current.generation === execution.generation ? retained : current.debugger;
|
|
465
|
+
const program = outcome.metadataChanged === true && current.generation === execution.generation ? outcome.savedSnapshot : undefined;
|
|
466
|
+
if (saved || program) {
|
|
467
|
+
if (program)
|
|
468
|
+
await this.workspace.artifacts.verify(program);
|
|
469
|
+
for (const trace of saved?.traces ?? [])
|
|
470
|
+
await this.workspace.artifacts.verify(trace.snapshot);
|
|
471
|
+
this.workspace.saveContext(selected, current.generation, { ...(saved ? { debugger: { ...saved, connected: false, traces: saved.traces.map(trace => ({ ...trace, connected: false })) } } : {}),
|
|
472
|
+
...(program ? { workingSnapshot: program, complexChanges: current.complexChanges.includes(request.id) ? current.complexChanges : [...current.complexChanges, request.id] } : {}) });
|
|
473
|
+
}
|
|
474
|
+
this.workspace.recordRequest(request.id, 'failed', { ...selected, state: 'disconnected', outcome: 'unknown', operation: execution.operation });
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch { /* A CPU action is not replayed or inferred from a later program counter. */ }
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (execution?.kind === 'ui') {
|
|
481
|
+
try {
|
|
482
|
+
if (backend.closed && await backend.closed(selected)) {
|
|
483
|
+
this.workspace.recordRequest(request.id, 'failed', { ...selected, state: 'disconnected', outcome: 'unknown', operation: execution.operation });
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (execution.operation === 'goto' && backend.ui) {
|
|
487
|
+
const p = object(object(request.intent).parameters), cursor = await backend.ui(selected, 'cursor', {}), location = object(cursor.address), current = this.workspace.context(selected);
|
|
488
|
+
if (location.session !== selected.session || location.target !== selected.target || address(location.address) !== address(p.address) || current.generation !== execution.generation)
|
|
489
|
+
continue;
|
|
490
|
+
const containing = cursor.function ? object(cursor.function) : undefined, value = { address: publicAddress(location.address), ...(containing && typeof containing.function_name === 'string' ? { function: containing.function_name } : {}) };
|
|
491
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, {}, { ...selected, state: 'applied', value });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
catch { /* Only the live verified cursor or a verified lost process establishes continuation. */ }
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (execution?.kind === 'export') {
|
|
498
|
+
try {
|
|
499
|
+
const snapshot = execution.snapshot;
|
|
500
|
+
await this.workspace.artifacts.verify(snapshot);
|
|
501
|
+
const observed = await this.workspace.artifacts.capture(text(execution.output, 4096), 'snapshot'), current = this.workspace.context(selected);
|
|
502
|
+
if (observed.id !== snapshot.id || current.generation !== execution.generation)
|
|
503
|
+
continue;
|
|
504
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, { workingSnapshot: snapshot }, { ...selected, state: 'exported', output: execution.output, sha256: snapshot.sha256, size: snapshot.size });
|
|
505
|
+
}
|
|
506
|
+
catch { /* Partial or changed output cannot prove an export and is never overwritten. */ }
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (execution?.kind === 'close' && backend.closed) {
|
|
510
|
+
try {
|
|
511
|
+
if (!await backend.closed(selected))
|
|
512
|
+
continue;
|
|
513
|
+
const current = this.workspace.context(selected);
|
|
514
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, {}, { ...selected, state: 'closed', pendingChanges: count(current) });
|
|
515
|
+
}
|
|
516
|
+
catch { /* A stopped metadata flag alone never proves process release. */ }
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (execution?.kind === 'save') {
|
|
520
|
+
try {
|
|
521
|
+
const snapshot = object(request.result).savedSnapshot;
|
|
522
|
+
if (!snapshot)
|
|
523
|
+
continue;
|
|
524
|
+
await this.workspace.artifacts.verify(snapshot);
|
|
525
|
+
const current = this.workspace.context(selected);
|
|
526
|
+
if (current.generation !== execution.generation)
|
|
527
|
+
continue;
|
|
528
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, { workingSnapshot: snapshot, viewEpoch: (current.viewEpoch ?? 0) + 1 }, { ...selected, state: 'saved', pendingChanges: count(current) });
|
|
529
|
+
}
|
|
530
|
+
catch { /* Unproved save outcome stays retained without another save dispatch. */ }
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
if (execution?.kind === 'transfer' && execution.dryRun === false && backend.transferCheck) {
|
|
534
|
+
try {
|
|
535
|
+
const sourceSnapshot = execution.sourceSnapshot, retained = request.result && object(request.result).savedSnapshot;
|
|
536
|
+
await this.workspace.artifacts.verify(sourceSnapshot);
|
|
537
|
+
await backend.open(selected, request.id);
|
|
538
|
+
const snapshot = retained ? retained : await backend.saved(selected);
|
|
539
|
+
await this.workspace.artifacts.verify(snapshot);
|
|
540
|
+
if (transferResult(await backend.transferCheck(selected, sourceSnapshot, snapshot), true).total !== 0)
|
|
541
|
+
continue;
|
|
542
|
+
const current = this.workspace.context(selected);
|
|
543
|
+
if (current.generation !== execution.generation && current.workingSnapshot?.id !== object(execution.workingSnapshot).id)
|
|
544
|
+
continue;
|
|
545
|
+
const scalars = await this.savedScalars(current, backend, snapshot);
|
|
546
|
+
const complexChanges = [...current.complexChanges, request.id];
|
|
547
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, { workingSnapshot: snapshot, complexChanges, scalars, viewEpoch: (current.viewEpoch ?? 0) + 1 }, { ...selected, source: execution.source, state: 'applied', reconciled: true, sharedState: 'pending', pendingChanges: scalars.length + complexChanges.length });
|
|
548
|
+
}
|
|
549
|
+
catch { /* Saved source/candidate remain retained; never execute transfer again as recovery. */ }
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (execution?.kind === 'program' && execution.savedVerification === true) {
|
|
553
|
+
try {
|
|
554
|
+
const retained = request.result && object(request.result).savedSnapshot;
|
|
555
|
+
let snapshot;
|
|
556
|
+
if (retained) {
|
|
557
|
+
snapshot = retained;
|
|
558
|
+
await this.workspace.artifacts.verify(snapshot);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
await backend.open(selected, request.id);
|
|
562
|
+
snapshot = await backend.saved(selected);
|
|
563
|
+
}
|
|
564
|
+
const intent = object(request.intent), parameters = object(intent.parameters);
|
|
565
|
+
const value = await verifyMutation(backend, snapshot, request.operation, parameters, {});
|
|
566
|
+
const current = this.workspace.context(selected);
|
|
567
|
+
if (current.generation !== execution.generation)
|
|
568
|
+
continue;
|
|
569
|
+
const scalars = await this.savedScalars(current, backend, snapshot), complexChanges = [...current.complexChanges, request.id];
|
|
570
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, { workingSnapshot: snapshot, complexChanges, scalars, viewEpoch: (current.viewEpoch ?? 0) + 1 }, { ...selected, state: 'applied', sharedState: 'pending', pendingChanges: scalars.length + complexChanges.length, value });
|
|
571
|
+
}
|
|
572
|
+
catch { /* Retain both snapshots and the unresolved intent when complete effects cannot be proven. */ }
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (execution?.kind === 'scalar') {
|
|
576
|
+
try {
|
|
577
|
+
await backend.open(selected, request.id);
|
|
578
|
+
const field = text(execution.field), at = address(execution.address);
|
|
579
|
+
const observed = field === 'function.name' ? String(functionResult(await backend.call(selected, '/get_functions', { functions: at, fields: 'entry_point', include_call_context: false }), at).name) : comments(await backend.call(selected, '/get_comment', { address: at }))[field.slice(8)];
|
|
580
|
+
if (observed !== execution.value)
|
|
581
|
+
continue;
|
|
582
|
+
const current = this.workspace.context(selected), snapshot = await backend.saved(selected);
|
|
583
|
+
const expected = number(execution.generation, Number.MAX_SAFE_INTEGER);
|
|
584
|
+
if (current.generation !== expected && current.workingSnapshot?.id !== object(execution.workingSnapshot).id)
|
|
585
|
+
continue;
|
|
586
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, { workingSnapshot: snapshot,
|
|
587
|
+
scalars: execution.scalars, complexChanges: execution.complexChanges }, execution.result);
|
|
588
|
+
}
|
|
589
|
+
catch { /* Never replay an uncertain setter merely because its desired value was not observed. */ }
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async merge(selected, callerId) {
|
|
594
|
+
const reserved = this.workspace.reserveRequest(selected, 'ghidra.merge', {}, callerId);
|
|
595
|
+
return this.ordered(selected, () => this.sharedOrder(selected.target, async () => {
|
|
596
|
+
let request = this.workspace.request(reserved.request.id), backend;
|
|
597
|
+
if (reserved.replay && ['running', 'unknown'].includes(request.state)) {
|
|
598
|
+
backend = await this.backend();
|
|
599
|
+
await this.reconcile(selected, backend);
|
|
600
|
+
request = this.workspace.request(request.id);
|
|
601
|
+
}
|
|
602
|
+
if (reserved.replay && request.state !== 'accepted') {
|
|
603
|
+
if (request.state === 'applied')
|
|
604
|
+
return request.result;
|
|
605
|
+
if (request.state === 'failed')
|
|
606
|
+
fail('conflict', 'This merge retained its original failed result.', request.result);
|
|
607
|
+
fail('unknown', 'This merge has an unresolved outcome; its intent must be reconciled.', request.result);
|
|
608
|
+
}
|
|
609
|
+
if (this.workspace.activePublication(selected.target) || this.workspace.model(selected.target).state === 'unknown')
|
|
610
|
+
fail('blocked', 'Shared model has an unresolved publication; preserve all private work until reconciliation.');
|
|
611
|
+
backend ??= await this.backend();
|
|
612
|
+
await backend.open(selected);
|
|
613
|
+
if (!backend.latest || !backend.fields || !backend.candidate || !backend.publish)
|
|
614
|
+
fail('configuration', 'Selected backend lacks the verified publication contract.');
|
|
615
|
+
let context = this.workspace.context(selected);
|
|
616
|
+
if (context.complexChanges.length && context.workingSnapshot) {
|
|
617
|
+
const scalars = await this.savedScalars(context, backend, context.workingSnapshot);
|
|
618
|
+
if (digest(scalars) !== digest(context.scalars))
|
|
619
|
+
context = this.workspace.saveContext(selected, context.generation, { scalars });
|
|
620
|
+
}
|
|
621
|
+
if (!context.baseRevision || !context.baseSnapshot || !context.workingSnapshot)
|
|
622
|
+
fail('binding', 'Private context has no saved comparison base.');
|
|
623
|
+
this.workspace.startRequest(request.id);
|
|
624
|
+
let publication;
|
|
625
|
+
try {
|
|
626
|
+
if (!count(context)) {
|
|
627
|
+
const result = { ...selected, state: 'empty', applied: 0, pendingChanges: 0 };
|
|
628
|
+
this.workspace.recordRequest(request.id, 'applied', result);
|
|
629
|
+
return result;
|
|
630
|
+
}
|
|
631
|
+
const shared = await backend.latest(selected.target);
|
|
632
|
+
this.workspace.observeShared(selected.target, shared.revision, shared.snapshot);
|
|
633
|
+
const original = await backend.fields(context.baseSnapshot, context.scalars), current = await backend.fields(shared.snapshot, context.scalars);
|
|
634
|
+
const comparisons = Object.fromEntries(context.scalars.map(change => {
|
|
635
|
+
const key = fieldKey(change), before = original[key], after = current[key];
|
|
636
|
+
if (!before || !after)
|
|
637
|
+
fail('partial', 'Saved comparison omitted a pending field.');
|
|
638
|
+
return [key, { value: after.value, compatible: after.exists && before.exists && (change.dependency ?? before.dependency) === after.dependency }];
|
|
639
|
+
}));
|
|
640
|
+
const plan = scalarMerge(context.scalars, context.complexChanges.length > 0, context.baseRevision, shared.revision, comparisons);
|
|
641
|
+
if (plan.state === 'conflict') {
|
|
642
|
+
const conflicts = [...plan.conflicts.map(conflict => ({ ...conflict, address: publicAddress(conflict.address) })),
|
|
643
|
+
...(context.complexChanges.length && context.baseRevision !== shared.revision ? [{ field: 'program', reason: 'complex_divergence',
|
|
644
|
+
originalRevision: revision(context.baseRevision), sharedRevision: revision(shared.revision), local: 'retained saved private state' }] : [])];
|
|
645
|
+
const scalars = context.scalars.map(change => {
|
|
646
|
+
const conflict = plan.conflicts.find(item => fieldKey(item) === fieldKey(change));
|
|
647
|
+
return conflict ? { ...change, conflict: { shared: conflict.shared } } : change;
|
|
648
|
+
});
|
|
649
|
+
this.workspace.saveContext(selected, context.generation, { scalars });
|
|
650
|
+
const result = { ...selected, state: 'conflict', applied: 0, sharedRevision: revision(shared.revision), pendingChanges: count(context), conflicts };
|
|
651
|
+
this.workspace.recordRequest(request.id, 'failed', result);
|
|
652
|
+
fail('conflict', 'Merge conflicts preserve all local work; nothing was published.', result);
|
|
653
|
+
}
|
|
654
|
+
if (plan.state === 'already_shared') {
|
|
655
|
+
const result = { ...selected, state: 'merged', applied: 0, sharedRevision: revision(shared.revision), pendingChanges: 0 };
|
|
656
|
+
this.workspace.completeSavedRequest(selected, request.id, context.generation, { baseRevision: shared.revision, baseSnapshot: shared.snapshot,
|
|
657
|
+
workingSnapshot: shared.snapshot, viewEpoch: (context.viewEpoch ?? 0) + 1, scalars: [], complexChanges: [] }, result);
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
const snapshot = plan.fullSnapshot ? context.workingSnapshot : await backend.candidate(selected.target, shared.snapshot, plan.apply);
|
|
661
|
+
publication = this.workspace.beginPublication(selected, request.id, shared.revision, snapshot);
|
|
662
|
+
publication = this.workspace.publicationIntent(publication.id);
|
|
663
|
+
const published = await backend.publish(publication);
|
|
664
|
+
if (published.state === 'conflict') {
|
|
665
|
+
const result = { ...selected, state: 'conflict', reason: 'shared_changed', applied: 0, pendingChanges: count(context) };
|
|
666
|
+
this.workspace.rejectPublication(publication.id, result);
|
|
667
|
+
fail('conflict', 'Shared base changed before publication; all local work was retained.', result);
|
|
668
|
+
}
|
|
669
|
+
const result = { ...selected, state: 'merged', sharedRevision: revision(published.revision), applied: count(context), pendingChanges: 0 };
|
|
670
|
+
this.workspace.finishPublication(publication.id, published.revision, result);
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
const state = this.workspace.request(request.id).state;
|
|
675
|
+
if (state !== 'applied' && state !== 'failed') {
|
|
676
|
+
if (publication && ['commit_intent', 'unknown'].includes(this.workspace.publication(publication.id).state))
|
|
677
|
+
this.workspace.unknownPublication(publication.id, { state: 'unknown' });
|
|
678
|
+
this.workspace.recordRequest(request.id, 'unknown', { ...selected, state: 'unknown' });
|
|
679
|
+
}
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
682
|
+
}));
|
|
683
|
+
}
|
|
684
|
+
async refresh(selected, callerId) {
|
|
685
|
+
const reserved = this.workspace.reserveRequest(selected, 'ghidra.refresh', {}, callerId);
|
|
686
|
+
return this.ordered(selected, () => this.sharedOrder(selected.target, async () => {
|
|
687
|
+
const request = this.workspace.request(reserved.request.id);
|
|
688
|
+
if (reserved.replay && request.state !== 'accepted') {
|
|
689
|
+
if (request.state === 'applied')
|
|
690
|
+
return request.result;
|
|
691
|
+
fail('unknown', 'Refresh retains an unresolved result; its saved state must be reconciled.', request.result);
|
|
692
|
+
}
|
|
693
|
+
if (this.workspace.activePublication(selected.target) || this.workspace.model(selected.target).state === 'unknown')
|
|
694
|
+
fail('blocked', 'Shared publication outcome must be reconciled before receiving its version.');
|
|
695
|
+
const backend = await this.backend();
|
|
696
|
+
await backend.open(selected);
|
|
697
|
+
if (!backend.latest || !backend.fields || !backend.candidate)
|
|
698
|
+
fail('configuration', 'Selected backend lacks the verified refresh contract.');
|
|
699
|
+
const context = this.workspace.context(selected);
|
|
700
|
+
if (!context.baseSnapshot || !context.baseRevision || !context.workingSnapshot)
|
|
701
|
+
fail('binding', 'Context lacks saved private and base state.');
|
|
702
|
+
this.workspace.startRequest(request.id);
|
|
703
|
+
try {
|
|
704
|
+
const shared = await backend.latest(selected.target);
|
|
705
|
+
this.workspace.observeShared(selected.target, shared.revision, shared.snapshot);
|
|
706
|
+
const publications = this.workspace.publishedSince(selected.target, context.baseRevision);
|
|
707
|
+
const fields = new Map(context.scalars.map(change => [fieldKey(change), change]));
|
|
708
|
+
for (const publication of publications)
|
|
709
|
+
for (const change of publication.scalars)
|
|
710
|
+
fields.set(fieldKey(change), change);
|
|
711
|
+
const selectors = [...fields.values()];
|
|
712
|
+
const original = await backend.fields(context.baseSnapshot, selectors), working = await backend.fields(context.workingSnapshot, selectors), current = await backend.fields(shared.snapshot, selectors);
|
|
713
|
+
const local = context.complexChanges.length ? await this.savedScalars(context, backend, context.workingSnapshot) : context.scalars.map(change => ({ ...change }));
|
|
714
|
+
// An opaque operation may have touched an incoming scalar too: retain that actual value.
|
|
715
|
+
if (context.complexChanges.length)
|
|
716
|
+
for (const change of selectors) {
|
|
717
|
+
const key = fieldKey(change), before = original[key], privateValue = working[key];
|
|
718
|
+
if (before && privateValue && before.value !== privateValue.value && !local.some(item => fieldKey(item) === key))
|
|
719
|
+
local.push({ address: change.address, field: change.field, original: before.value, local: privateValue.value, dependency: before.dependency, ...(!privateValue.exists ? { removed: true } : {}) });
|
|
720
|
+
}
|
|
721
|
+
const incoming = [], comparisons = {};
|
|
722
|
+
for (const selector of selectors) {
|
|
723
|
+
const key = fieldKey(selector), before = original[key], privateValue = working[key], after = current[key];
|
|
724
|
+
if (!before || !privateValue || !after)
|
|
725
|
+
fail('partial', 'Refresh comparison omitted an incoming or local field.');
|
|
726
|
+
comparisons[key] = { value: after.value, compatible: before.exists && privateValue.exists && after.exists &&
|
|
727
|
+
before.dependency === after.dependency && privateValue.dependency === after.dependency };
|
|
728
|
+
incoming.push({ address: selector.address, field: selector.field, original: before.value, local: after.value });
|
|
729
|
+
}
|
|
730
|
+
const plan = scalarRefresh(local, incoming, comparisons);
|
|
731
|
+
const changed = plan.receive.filter(change => working[fieldKey(change)]?.value !== change.local);
|
|
732
|
+
const dependencyConflict = local.some(change => !comparisons[fieldKey(change)]?.compatible);
|
|
733
|
+
const preserveWhole = context.complexChanges.length > 0 || dependencyConflict;
|
|
734
|
+
let snapshot = context.workingSnapshot;
|
|
735
|
+
if (preserveWhole) {
|
|
736
|
+
if (changed.length)
|
|
737
|
+
snapshot = await backend.candidate(selected.target, context.workingSnapshot, changed);
|
|
738
|
+
}
|
|
739
|
+
else
|
|
740
|
+
snapshot = plan.preserve.length ? await backend.candidate(selected.target, shared.snapshot, plan.preserve) : shared.snapshot;
|
|
741
|
+
const complexConflict = preserveWhole && shared.revision !== context.baseRevision;
|
|
742
|
+
const conflicts = [...plan.conflicts.map(conflict => ({ ...conflict, address: publicAddress(conflict.address) })),
|
|
743
|
+
...(complexConflict ? [{ field: 'program', reason: 'complex_divergence', originalRevision: revision(context.baseRevision),
|
|
744
|
+
sharedRevision: revision(shared.revision), local: 'retained saved private state' }] : [])];
|
|
745
|
+
const receivedProgram = !preserveWhole && shared.revision !== context.baseRevision &&
|
|
746
|
+
(publications.some(publication => publication.complex) || publications.length !== shared.revision - context.baseRevision);
|
|
747
|
+
const result = { ...selected, state: conflicts.length ? 'conflict' : 'updated', sharedRevision: revision(shared.revision),
|
|
748
|
+
receivedChanges: changed.length + Number(receivedProgram), preservedLocalChanges: plan.preserve.length + context.complexChanges.length, conflicts };
|
|
749
|
+
this.workspace.completeSavedRequest(selected, request.id, context.generation, { workingSnapshot: snapshot, viewEpoch: (context.viewEpoch ?? 0) + 1,
|
|
750
|
+
scalars: plan.preserve, ...(!preserveWhole ? { baseRevision: shared.revision, baseSnapshot: shared.snapshot } : {}) }, result);
|
|
751
|
+
return result;
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
if (this.workspace.request(request.id).state !== 'applied')
|
|
755
|
+
this.workspace.recordRequest(request.id, 'unknown', { ...selected, state: 'unknown' });
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
}));
|
|
759
|
+
}
|
|
760
|
+
async mutate(selected, operation, parameters, verify, callerId, verifySaved) {
|
|
761
|
+
const reserved = this.workspace.reserveRequest(selected, operation, parameters, callerId);
|
|
762
|
+
return this.ordered(selected, async () => {
|
|
763
|
+
const request = this.workspace.request(reserved.request.id);
|
|
764
|
+
if (reserved.replay && request.state !== 'accepted') {
|
|
765
|
+
if (request.state === 'applied')
|
|
766
|
+
return request.result;
|
|
767
|
+
if (operation.startsWith('/sdk/') && request.state === 'failed' && object(request.result ?? {}).state === 'rejected')
|
|
768
|
+
fail('rejected', 'This SDK transaction was rolled back and is not repeated.', request.result);
|
|
769
|
+
fail('unknown', 'This saved-state operation retains an unresolved outcome; it is not re-executed.', request.result);
|
|
770
|
+
}
|
|
771
|
+
const backend = await this.backend();
|
|
772
|
+
await backend.open(selected);
|
|
773
|
+
if (operation === '/batch_apply_documentation' && parameters.goto === true)
|
|
774
|
+
await backend.mode(selected, 'gui');
|
|
775
|
+
const context = this.workspace.context(selected);
|
|
776
|
+
this.workspace.startRequest(request.id, { kind: 'program', operation, generation: context.generation, workingSnapshot: context.workingSnapshot,
|
|
777
|
+
savedVerification: !!verifySaved });
|
|
778
|
+
try {
|
|
779
|
+
const raw = await backend.call(selected, operation, parameters), response = raw && typeof raw === 'object' && !Array.isArray(raw) ? object(raw) : { output: raw };
|
|
780
|
+
if (['rejected', 'partial', 'failed', 'error'].includes(String(response.status)) || response.success === false)
|
|
781
|
+
fail('partial', 'Program operation reported a rejected or incomplete effect.');
|
|
782
|
+
if (!verifySaved && response.success !== true && response.status !== 'success')
|
|
783
|
+
fail('partial', 'Program operation did not acknowledge its effect.');
|
|
784
|
+
let value = await verify(backend, response);
|
|
785
|
+
const snapshot = await backend.saved(selected);
|
|
786
|
+
if (verifySaved)
|
|
787
|
+
value = await verifySaved(backend, snapshot, response);
|
|
788
|
+
const scalars = await this.savedScalars(context, backend, snapshot), complexChanges = [...context.complexChanges, request.id], result = { ...selected, state: 'applied', sharedState: 'pending',
|
|
789
|
+
pendingChanges: scalars.length + complexChanges.length, value };
|
|
790
|
+
this.workspace.completeSavedRequest(selected, request.id, context.generation, { workingSnapshot: snapshot, complexChanges, scalars }, result);
|
|
791
|
+
return result;
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
if (operation.startsWith('/sdk/') && String(error.code).endsWith('.rejected')) {
|
|
795
|
+
this.workspace.recordRequest(request.id, 'failed', { ...selected, state: 'rejected', reason: 'native_transaction_rolled_back' });
|
|
796
|
+
throw error;
|
|
797
|
+
}
|
|
798
|
+
let savedSnapshot;
|
|
799
|
+
try {
|
|
800
|
+
savedSnapshot = await backend.saved(selected);
|
|
801
|
+
}
|
|
802
|
+
catch { /* A failed/dead backend retains its native stage and the last acknowledged snapshot. */ }
|
|
803
|
+
this.workspace.recordRequest(request.id, 'unknown', { ...selected, state: 'unknown', ...(savedSnapshot ? { savedSnapshot } : {}) });
|
|
804
|
+
throw error;
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|