@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,154 @@
|
|
|
1
|
+
import { command, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { scope } from './research.js';
|
|
3
|
+
import { id, fail, object, digest } from './model.js';
|
|
4
|
+
import { selectedOptions } from './arguments.js';
|
|
5
|
+
export class Lifecycle {
|
|
6
|
+
workspace;
|
|
7
|
+
research;
|
|
8
|
+
backend;
|
|
9
|
+
constructor(workspace, research, backend) {
|
|
10
|
+
this.workspace = workspace;
|
|
11
|
+
this.research = research;
|
|
12
|
+
this.backend = backend;
|
|
13
|
+
}
|
|
14
|
+
async health(selected) {
|
|
15
|
+
return this.research.ordered(selected, async () => {
|
|
16
|
+
const backend = await this.backend();
|
|
17
|
+
await backend.open(selected);
|
|
18
|
+
const connection = object(await backend.call(selected, '/check_connection')), health = object(await backend.call(selected, '/mcp/health'));
|
|
19
|
+
if (connection.status !== 'ok' || health.status !== 'ok' || health.connected !== true || !['headless', 'gui'].includes(String(health.server_kind)))
|
|
20
|
+
fail('partial', 'Tool health did not verify the selected context.');
|
|
21
|
+
return { ...this.research.status(selected), health: { connected: true, mode: health.server_kind, version: health.version, memory: health.memory_mb } };
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async change(selected, operation, callerId) {
|
|
25
|
+
const reserved = this.workspace.reserveRequest(selected, 'ghidra.' + operation, {}, callerId);
|
|
26
|
+
return this.research.ordered(selected, async () => {
|
|
27
|
+
const request = this.workspace.request(reserved.request.id);
|
|
28
|
+
if (reserved.replay && request.state !== 'accepted') {
|
|
29
|
+
if (request.state === 'applied')
|
|
30
|
+
return request.result;
|
|
31
|
+
fail('unknown', 'This lifecycle outcome is retained; it is not re-executed.', request.result);
|
|
32
|
+
}
|
|
33
|
+
const backend = await this.backend(), unresolved = this.workspace.unresolved(selected);
|
|
34
|
+
const closingExternal = operation === 'close' && unresolved.length > 0 && unresolved.every(intent => ['debugger', 'ui'].includes(String(object(intent.execution ?? {}).kind)));
|
|
35
|
+
const alreadyEnded = closingExternal && backend.closed ? await backend.closed(selected) : false;
|
|
36
|
+
if (!closingExternal)
|
|
37
|
+
await backend.open(selected);
|
|
38
|
+
const before = this.workspace.context(selected);
|
|
39
|
+
this.workspace.startRequest(request.id, { kind: operation, generation: before.generation, workingSnapshot: before.workingSnapshot });
|
|
40
|
+
let savedSnapshot;
|
|
41
|
+
try {
|
|
42
|
+
if (operation === 'close' && !alreadyEnded)
|
|
43
|
+
await backend.close(selected);
|
|
44
|
+
const current = this.workspace.context(selected), snapshot = operation === 'save' ? await backend.saved(selected) : current.workingSnapshot;
|
|
45
|
+
if (!snapshot)
|
|
46
|
+
fail('partial', 'Lifecycle operation did not retain its private snapshot.');
|
|
47
|
+
savedSnapshot = snapshot;
|
|
48
|
+
if (closingExternal && backend.closed && await backend.closed(selected))
|
|
49
|
+
for (const intent of unresolved) {
|
|
50
|
+
const before = this.workspace.context(selected), execution = object(intent.execution ?? {}), outcome = object(intent.result ?? {}), retained = outcome.savedDebugger;
|
|
51
|
+
if (execution.kind === 'debugger' && before.generation === execution.generation) {
|
|
52
|
+
const program = outcome.metadataChanged === true ? outcome.savedSnapshot : undefined;
|
|
53
|
+
if (program)
|
|
54
|
+
await this.workspace.artifacts.verify(program);
|
|
55
|
+
for (const trace of retained?.traces ?? [])
|
|
56
|
+
await this.workspace.artifacts.verify(trace.snapshot);
|
|
57
|
+
if (retained || program)
|
|
58
|
+
this.workspace.saveContext(selected, before.generation, { ...(retained ? { debugger: { ...retained, connected: false, traces: retained.traces.map(trace => ({ ...trace, connected: false })) } } : {}),
|
|
59
|
+
...(program ? { workingSnapshot: program, complexChanges: before.complexChanges.includes(intent.id) ? before.complexChanges : [...before.complexChanges, intent.id] } : {}) });
|
|
60
|
+
}
|
|
61
|
+
this.workspace.recordRequest(intent.id, 'failed', { ...selected, state: 'disconnected', outcome: 'unknown', operation: object(intent.execution ?? {}).operation });
|
|
62
|
+
}
|
|
63
|
+
const retained = this.workspace.context(selected);
|
|
64
|
+
const result = { ...selected, state: operation === 'save' ? 'saved' : 'closed', pendingChanges: retained.scalars.length + retained.complexChanges.length };
|
|
65
|
+
this.workspace.completeSavedRequest(selected, request.id, retained.generation, { workingSnapshot: operation === 'close' ? retained.workingSnapshot ?? snapshot : snapshot, ...(operation === 'close' ? { state: 'stopped',
|
|
66
|
+
...(retained.debugger ? { debugger: { ...retained.debugger, connected: false, traces: retained.debugger.traces.map(trace => ({ ...trace, connected: false })) } } : {}) } : {}) }, result);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.workspace.recordRequest(request.id, 'unknown', { ...selected, state: 'unknown', ...(savedSnapshot ? { savedSnapshot } : {}) });
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async discard(selected, callerId) {
|
|
76
|
+
const reserved = this.workspace.reserveRequest(selected, 'ghidra.changes.discard', {}, callerId);
|
|
77
|
+
return this.research.ordered(selected, async () => {
|
|
78
|
+
const request = this.workspace.request(reserved.request.id);
|
|
79
|
+
if (reserved.replay && request.state !== 'accepted') {
|
|
80
|
+
if (request.state === 'applied')
|
|
81
|
+
return request.result;
|
|
82
|
+
fail('unknown', 'Local discard outcome is retained; continue the context before retrieving it.', request.result);
|
|
83
|
+
}
|
|
84
|
+
const backend = await this.backend();
|
|
85
|
+
await backend.open(selected);
|
|
86
|
+
if (this.workspace.context(selected).debugger?.connected)
|
|
87
|
+
fail('busy', 'Close the live debugger context before discarding static program edits.');
|
|
88
|
+
// Resource save/release precedes the local atomic switch, as with mode preparation.
|
|
89
|
+
await backend.close(selected);
|
|
90
|
+
const context = this.workspace.context(selected);
|
|
91
|
+
if (!context.baseSnapshot || !context.workingSnapshot)
|
|
92
|
+
fail('binding', 'Discard requires retained base and private snapshots.');
|
|
93
|
+
await this.workspace.artifacts.verify(context.baseSnapshot);
|
|
94
|
+
const result = { ...selected, state: 'discarded', discardedChanges: context.scalars.length + context.complexChanges.length, pendingChanges: 0,
|
|
95
|
+
baseRevision: 'R' + context.baseRevision, discardedSnapshot: context.workingSnapshot };
|
|
96
|
+
this.workspace.startRequest(request.id, { kind: 'discard', generation: context.generation, baseSnapshot: context.baseSnapshot, discardedSnapshot: context.workingSnapshot, result });
|
|
97
|
+
this.workspace.completeSavedRequest(selected, request.id, context.generation, { workingSnapshot: context.baseSnapshot, state: 'stopped', viewEpoch: (context.viewEpoch ?? 0) + 1, scalars: [], complexChanges: [] }, result);
|
|
98
|
+
return result;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async saveAll(session, callerId) {
|
|
102
|
+
this.workspace.session(session);
|
|
103
|
+
const reserved = this.workspace.reserveAdministration('session.ghidra.save', { session }, callerId), request = this.workspace.administration(reserved.request.id);
|
|
104
|
+
if (reserved.replay && request.state !== 'accepted') {
|
|
105
|
+
if (request.state === 'applied')
|
|
106
|
+
return request.result;
|
|
107
|
+
fail('unknown', 'Session save outcomes are retained and are not repeated.', request.result);
|
|
108
|
+
}
|
|
109
|
+
const contexts = [];
|
|
110
|
+
for (let offset = 0;; offset += 100) {
|
|
111
|
+
const page = this.workspace.contexts(session, 100, offset);
|
|
112
|
+
contexts.push(...page);
|
|
113
|
+
if (page.length < 100)
|
|
114
|
+
break;
|
|
115
|
+
if (contexts.length >= 1000)
|
|
116
|
+
fail('limit', 'Session-wide save supports fewer than 1000 contexts; save explicitly selected contexts.');
|
|
117
|
+
}
|
|
118
|
+
const result = { session, state: 'saving', saved: 0, errors: 0, items: [] };
|
|
119
|
+
this.workspace.administrationState(request.id, 'running', result);
|
|
120
|
+
for (const context of contexts) {
|
|
121
|
+
const selected = { session, target: context.target };
|
|
122
|
+
this.workspace.administrationState(request.id, 'unknown', { ...result, pending: selected });
|
|
123
|
+
try {
|
|
124
|
+
const value = object(await this.change(selected, 'save', 'save-' + digest([request.id, context.id])));
|
|
125
|
+
result.items.push(value);
|
|
126
|
+
result.saved++;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
result.items.push({ ...selected, state: 'unknown' });
|
|
130
|
+
result.errors++;
|
|
131
|
+
}
|
|
132
|
+
this.workspace.administrationState(request.id, 'unknown', result);
|
|
133
|
+
}
|
|
134
|
+
result.state = result.errors ? 'partial' : 'saved';
|
|
135
|
+
this.workspace.administrationState(request.id, result.errors ? 'unknown' : 'applied', result);
|
|
136
|
+
if (result.errors)
|
|
137
|
+
fail('partial', 'Some selected context saves need recovery; completed saves remain retained.', result);
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function lifecycleCommands(service, runtimeStatus) {
|
|
142
|
+
return [command('changes', 'Manage unpublished edits in the selected context', [
|
|
143
|
+
command('discard', 'Save a retained copy, then restore the opening base without publication', ({ options: o }, c) => service(c).discard(scope(o), o.requestId === undefined ? undefined : id(o.requestId)), { permission: 'Destructive', options: [...selectedOptions, { flags: '--request-id <id>', description: 'Retrieve the same local discard result' }] })
|
|
144
|
+
]),
|
|
145
|
+
command('save-all', 'Save every Ghidra context in the explicit session; outcomes are per context', ({ options: o }, c) => service(c).saveAll(id(o.session), o.requestId === undefined ? undefined : id(o.requestId)), { permission: Permission.Update, options: [{ flags: '--session <id>', description: 'Explicit session whose contexts are saved', required: true }, { flags: '--request-id <id>', description: 'Retrieve the recorded session save outcomes' }] }),
|
|
146
|
+
command('status', 'Inspect the installed runtime, or verify a selected context', ({ options: o }, c) => {
|
|
147
|
+
if (o.session === undefined && o.target === undefined && runtimeStatus)
|
|
148
|
+
return runtimeStatus(c);
|
|
149
|
+
if (o.session === undefined || o.target === undefined)
|
|
150
|
+
fail('invalid', 'Context status requires both --session and --target.');
|
|
151
|
+
return service(c).health(scope(o));
|
|
152
|
+
}, { permission: Permission.ReadOnly, options: selectedOptions.map(option => ({ ...option, required: false })) }),
|
|
153
|
+
...['save', 'close'].map(name => command(name, name === 'save' ? 'Save private work without publication' : 'Save and release the owned context; reopen retains local work', ({ options: o }, c) => service(c).change(scope(o), name, o.requestId === undefined ? undefined : id(o.requestId)), { permission: Permission.Update, options: [...selectedOptions, { flags: '--request-id <id>', description: 'Retrieve the lifecycle outcome after a lost response' }] }))];
|
|
154
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ResearchBackend } from './research.js';
|
|
2
|
+
import type { Artifact } from './artifacts.js';
|
|
3
|
+
export declare function blockContent(p: Record<string, unknown>): {
|
|
4
|
+
size: number;
|
|
5
|
+
initialized: boolean;
|
|
6
|
+
sha256: string | null;
|
|
7
|
+
};
|
|
8
|
+
export declare function verifyMemoryCreation(backend: ResearchBackend, snapshot: Artifact, p: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { fail, object } from './model.js';
|
|
3
|
+
import { address, number } from './research.js';
|
|
4
|
+
export function blockContent(p) {
|
|
5
|
+
let bytes;
|
|
6
|
+
if (p.bytes_hex !== undefined && p.bytes_base64 !== undefined)
|
|
7
|
+
fail('invalid', 'Choose bytes-hex or bytes-base64, not both.');
|
|
8
|
+
if (p.bytes_hex !== undefined) {
|
|
9
|
+
const value = String(p.bytes_hex).replace(/\s/g, '');
|
|
10
|
+
if (!value || value.length > 33554432 || value.length % 2 || !/^[0-9a-f]+$/i.test(value))
|
|
11
|
+
fail('invalid', 'bytes-hex needs complete hex byte pairs, at most 16 MiB.');
|
|
12
|
+
bytes = Buffer.from(value, 'hex');
|
|
13
|
+
}
|
|
14
|
+
if (p.bytes_base64 !== undefined) {
|
|
15
|
+
const value = String(p.bytes_base64);
|
|
16
|
+
if (!value || value.length > 22369624 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value))
|
|
17
|
+
fail('invalid', 'bytes-base64 needs canonical padded base64, at most 16 MiB.');
|
|
18
|
+
bytes = Buffer.from(value, 'base64');
|
|
19
|
+
if (bytes.toString('base64') !== value)
|
|
20
|
+
fail('invalid', 'bytes-base64 is not canonical.');
|
|
21
|
+
}
|
|
22
|
+
const initialized = p.initialized !== false, fill = number(p.fill_byte ?? 0, 255), size = number(p.size ?? bytes?.length, 16777216, 1);
|
|
23
|
+
if (bytes && bytes.length > size)
|
|
24
|
+
fail('invalid', 'Initial content exceeds the requested block size.');
|
|
25
|
+
if (!initialized && (bytes || fill !== 0))
|
|
26
|
+
fail('invalid', 'Uninitialized blocks cannot have byte content or a nonzero fill byte.');
|
|
27
|
+
const content = initialized ? Buffer.alloc(size, fill) : undefined;
|
|
28
|
+
if (content && bytes)
|
|
29
|
+
bytes.copy(content);
|
|
30
|
+
return { size, initialized, sha256: content ? createHash('sha256').update(content).digest('hex') : null };
|
|
31
|
+
}
|
|
32
|
+
export async function verifyMemoryCreation(backend, snapshot, p) {
|
|
33
|
+
if (!backend.inspect)
|
|
34
|
+
fail('configuration', 'Memory creation needs immutable saved-state inspection.');
|
|
35
|
+
const expected = blockContent(p), value = object(await backend.inspect(snapshot, { kind: 'memory', name: p.name, digest_bytes: true }));
|
|
36
|
+
const equal = (actual, wanted, label) => { if (actual !== wanted)
|
|
37
|
+
fail('partial', `Saved memory ${label} differs from the requested effect.`); };
|
|
38
|
+
equal(value.exists, true, 'presence');
|
|
39
|
+
equal(value.name, p.name, 'name');
|
|
40
|
+
equal(value.size, expected.size, 'size');
|
|
41
|
+
equal(value.initialized, expected.initialized, 'initialization');
|
|
42
|
+
equal(value.overlay, p.overlay ?? false, 'overlay');
|
|
43
|
+
for (const flag of ['read', 'write', 'execute', 'volatile'])
|
|
44
|
+
equal(value[flag], p[flag] ?? ['read', 'write'].includes(flag), flag);
|
|
45
|
+
equal(value.comment ?? '', p.comment ?? '', 'comment');
|
|
46
|
+
equal(value.sha256, expected.sha256, 'content digest');
|
|
47
|
+
const at = address(p.address), actual = String(value.start);
|
|
48
|
+
if (p.overlay) {
|
|
49
|
+
equal(value.physicalStart !== undefined ? address(value.physicalStart) : undefined, at, 'physical start');
|
|
50
|
+
if (!actual.includes(':'))
|
|
51
|
+
fail('partial', 'Saved overlay lost its address-space identity.');
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
equal(address(actual), at, 'start');
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ScalarChange } from './workspace.js';
|
|
2
|
+
export declare const fieldKey: (change: Pick<ScalarChange, "address" | "field">) => string;
|
|
3
|
+
export interface SharedScalar {
|
|
4
|
+
value: string | null;
|
|
5
|
+
compatible: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface FieldConflict {
|
|
8
|
+
address: string;
|
|
9
|
+
field: ScalarChange['field'];
|
|
10
|
+
original: string | null;
|
|
11
|
+
local: string | null;
|
|
12
|
+
shared: string | null;
|
|
13
|
+
reason: 'overlap' | 'dependency_changed';
|
|
14
|
+
}
|
|
15
|
+
export interface MergePlan {
|
|
16
|
+
state: 'publish' | 'conflict' | 'already_shared';
|
|
17
|
+
apply: ScalarChange[];
|
|
18
|
+
conflicts: FieldConflict[];
|
|
19
|
+
fullSnapshot: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Three-way decisions only; backend effects happen after the entire plan passes. */
|
|
22
|
+
export declare function scalarMerge(changes: ScalarChange[], complex: boolean, baseRevision: number, sharedRevision: number, values: Record<string, SharedScalar>): MergePlan;
|
|
23
|
+
export interface RefreshPlan {
|
|
24
|
+
receive: ScalarChange[];
|
|
25
|
+
preserve: ScalarChange[];
|
|
26
|
+
conflicts: FieldConflict[];
|
|
27
|
+
}
|
|
28
|
+
/** Incoming safe fields update a private view; conflicting local values remain explicit. */
|
|
29
|
+
export declare function scalarRefresh(local: ScalarChange[], incoming: ScalarChange[], values: Record<string, SharedScalar>): RefreshPlan;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { fail } from './model.js';
|
|
2
|
+
export const fieldKey = (change) => change.address + '@' + change.field;
|
|
3
|
+
/** Three-way decisions only; backend effects happen after the entire plan passes. */
|
|
4
|
+
export function scalarMerge(changes, complex, baseRevision, sharedRevision, values) {
|
|
5
|
+
if (sharedRevision < baseRevision)
|
|
6
|
+
fail('binding', 'Shared history regressed below the private baseline.');
|
|
7
|
+
// A verified complete private result can include fields changed by structural edits.
|
|
8
|
+
// Retained refresh conflicts still require their original/local/shared comparison.
|
|
9
|
+
if (complex && baseRevision === sharedRevision && !changes.some(change => change.conflict))
|
|
10
|
+
return { state: 'publish', apply: [...changes], conflicts: [], fullSnapshot: true };
|
|
11
|
+
const apply = [], conflicts = [];
|
|
12
|
+
for (const change of changes) {
|
|
13
|
+
const observed = values[fieldKey(change)];
|
|
14
|
+
if (!observed)
|
|
15
|
+
fail('partial', 'Shared comparison omitted a local field.');
|
|
16
|
+
if (!observed.compatible)
|
|
17
|
+
conflicts.push({ ...change, shared: observed.value, reason: 'dependency_changed' });
|
|
18
|
+
else if (observed.value === change.local)
|
|
19
|
+
continue;
|
|
20
|
+
else if (observed.value === change.original)
|
|
21
|
+
apply.push(change);
|
|
22
|
+
else
|
|
23
|
+
conflicts.push({ ...change, shared: observed.value, reason: 'overlap' });
|
|
24
|
+
}
|
|
25
|
+
if (complex && baseRevision !== sharedRevision)
|
|
26
|
+
return { state: 'conflict', apply: [], conflicts, fullSnapshot: true };
|
|
27
|
+
if (conflicts.length)
|
|
28
|
+
return { state: 'conflict', apply: [], conflicts, fullSnapshot: false };
|
|
29
|
+
return { state: apply.length || complex ? 'publish' : 'already_shared', apply, conflicts: [], fullSnapshot: baseRevision === sharedRevision };
|
|
30
|
+
}
|
|
31
|
+
/** Incoming safe fields update a private view; conflicting local values remain explicit. */
|
|
32
|
+
export function scalarRefresh(local, incoming, values) {
|
|
33
|
+
const locals = new Map(local.map(change => [fieldKey(change), change]));
|
|
34
|
+
const preserve = local.map(change => ({ ...change })), receive = [], conflicts = [];
|
|
35
|
+
for (const change of incoming) {
|
|
36
|
+
const key = fieldKey(change), pending = locals.get(key), observed = values[key];
|
|
37
|
+
if (!observed)
|
|
38
|
+
fail('partial', 'Incoming comparison omitted a field.');
|
|
39
|
+
if (!observed.compatible) {
|
|
40
|
+
if (pending)
|
|
41
|
+
conflicts.push({ ...pending, shared: observed.value, reason: 'dependency_changed' });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!pending) {
|
|
45
|
+
receive.push({ ...change, local: observed.value });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (observed.value === pending.local) {
|
|
49
|
+
const index = preserve.findIndex(item => fieldKey(item) === key);
|
|
50
|
+
preserve.splice(index, 1);
|
|
51
|
+
}
|
|
52
|
+
else if (observed.value !== pending.original)
|
|
53
|
+
conflicts.push({ ...pending, shared: observed.value, reason: 'overlap' });
|
|
54
|
+
else {
|
|
55
|
+
const preserved = preserve.find(item => fieldKey(item) === key);
|
|
56
|
+
if (preserved)
|
|
57
|
+
delete preserved.conflict;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const conflict of conflicts) {
|
|
61
|
+
const preserved = preserve.find(change => fieldKey(change) === fieldKey(conflict));
|
|
62
|
+
if (preserved)
|
|
63
|
+
preserved.conflict = { shared: conflict.shared };
|
|
64
|
+
}
|
|
65
|
+
return { receive, preserve, conflicts };
|
|
66
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export type State = 'queued' | 'applying' | 'staged' | 'checking_in' | 'publication_unknown' | 'published_verified' | 'conflict_pending' | 'failed';
|
|
2
|
+
export interface Identity {
|
|
3
|
+
profile: string;
|
|
4
|
+
server: string;
|
|
5
|
+
repository: string;
|
|
6
|
+
programPath: string;
|
|
7
|
+
binarySha256: string;
|
|
8
|
+
language: string;
|
|
9
|
+
compilerSpec: string;
|
|
10
|
+
imageBase: string;
|
|
11
|
+
}
|
|
12
|
+
export interface Target extends Identity {
|
|
13
|
+
id: string;
|
|
14
|
+
lane: string;
|
|
15
|
+
}
|
|
16
|
+
export type CommentKind = 'eol' | 'pre' | 'post' | 'plate' | 'repeatable';
|
|
17
|
+
export interface Finding {
|
|
18
|
+
requestId: string;
|
|
19
|
+
findingId: string;
|
|
20
|
+
targetId: string;
|
|
21
|
+
identity: Identity;
|
|
22
|
+
workerId: string;
|
|
23
|
+
baselineVersion: number;
|
|
24
|
+
address: string;
|
|
25
|
+
kind: CommentKind;
|
|
26
|
+
before: string | null;
|
|
27
|
+
after: string;
|
|
28
|
+
evidence: string;
|
|
29
|
+
resolves?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface FindingRecord extends Finding {
|
|
32
|
+
state: State;
|
|
33
|
+
batchId?: string;
|
|
34
|
+
conflict?: unknown;
|
|
35
|
+
}
|
|
36
|
+
export interface Binding {
|
|
37
|
+
pid: number;
|
|
38
|
+
endpoint: string;
|
|
39
|
+
projectDirectory: string;
|
|
40
|
+
identity: Identity;
|
|
41
|
+
version: number;
|
|
42
|
+
role: 'reader' | 'publisher';
|
|
43
|
+
bridgeSecret?: string;
|
|
44
|
+
bridgePid?: number;
|
|
45
|
+
readEpoch?: string;
|
|
46
|
+
openingVersion?: number;
|
|
47
|
+
}
|
|
48
|
+
export interface Batch {
|
|
49
|
+
id: string;
|
|
50
|
+
targetId: string;
|
|
51
|
+
state: State;
|
|
52
|
+
ownerPid: number;
|
|
53
|
+
binding?: Binding;
|
|
54
|
+
baselineVersion?: number;
|
|
55
|
+
publishedVersion?: number;
|
|
56
|
+
findings: FindingRecord[];
|
|
57
|
+
detail?: unknown;
|
|
58
|
+
}
|
|
59
|
+
export interface Snapshot {
|
|
60
|
+
identity: Identity;
|
|
61
|
+
version: number;
|
|
62
|
+
values: Record<string, string | null>;
|
|
63
|
+
}
|
|
64
|
+
export declare const key: (f: Pick<Finding, "address" | "kind">) => string;
|
|
65
|
+
export declare const stable: (value: unknown) => string;
|
|
66
|
+
export declare const digest: (value: unknown) => string;
|
|
67
|
+
export declare function fail(code: string, message: string, result?: unknown): never;
|
|
68
|
+
export declare function object(value: unknown): Record<string, unknown>;
|
|
69
|
+
export declare function text(value: unknown, max?: number): string;
|
|
70
|
+
export declare function id(value: unknown): string;
|
|
71
|
+
export declare function version(value: unknown): number;
|
|
72
|
+
export declare function identity(value: unknown): Identity;
|
|
73
|
+
export declare function assertIdentity(actual: Identity, expected: Identity): void;
|
|
74
|
+
export declare function finding(value: unknown): Finding;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { CliError } from '@eyeauras/cli-factory';
|
|
3
|
+
export const key = (f) => `${f.address}:${f.kind}`;
|
|
4
|
+
export const stable = (value) => JSON.stringify(value, (_key, item) => item && typeof item === 'object' && !Array.isArray(item)
|
|
5
|
+
? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b))) : item);
|
|
6
|
+
export const digest = (value) => createHash('sha256').update(stable(value)).digest('hex');
|
|
7
|
+
export function fail(code, message, result) {
|
|
8
|
+
throw new CliError(message, { code: `coordinator.${code}`, ...(result === undefined ? {} : { result }) });
|
|
9
|
+
}
|
|
10
|
+
export function object(value) {
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
12
|
+
fail('invalid', 'Expected a JSON object.');
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
export function text(value, max = 512) {
|
|
16
|
+
if (typeof value !== 'string' || !value.length || value.length > max || /[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(value))
|
|
17
|
+
fail('invalid', 'Expected a bounded non-empty string.');
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
export function id(value) {
|
|
21
|
+
const result = text(value, 128);
|
|
22
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(result))
|
|
23
|
+
fail('invalid', 'ID must use letters, digits, dot, underscore or hyphen.');
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
export function version(value) {
|
|
27
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 1)
|
|
28
|
+
fail('invalid', 'Version must be a positive integer.');
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
export function identity(value) {
|
|
32
|
+
const o = object(value);
|
|
33
|
+
const result = {
|
|
34
|
+
profile: text(o.profile), server: text(o.server), repository: text(o.repository),
|
|
35
|
+
programPath: text(o.programPath), binarySha256: text(o.binarySha256).toLowerCase(),
|
|
36
|
+
language: text(o.language), compilerSpec: text(o.compilerSpec), imageBase: text(o.imageBase).toLowerCase(),
|
|
37
|
+
};
|
|
38
|
+
if (!/^ghidra:\/\/[^/]+$/.test(result.server) || /[@?#]/.test(result.server))
|
|
39
|
+
fail('invalid', 'Server must be a ghidra://host:port origin without credentials.');
|
|
40
|
+
if (/[\\/]/.test(result.repository) || result.repository.startsWith('.'))
|
|
41
|
+
fail('invalid', 'Repository name must be a single non-hidden component.');
|
|
42
|
+
if (!result.programPath.startsWith('/') || result.programPath.split('/').slice(1).some(p => !p || p.startsWith('.')))
|
|
43
|
+
fail('invalid', 'Program path must be absolute with non-hidden components.');
|
|
44
|
+
if (!/^[a-f0-9]{64}$/.test(result.binarySha256) || !/^[a-f0-9]+$/.test(result.imageBase))
|
|
45
|
+
fail('invalid', 'SHA256 or image base is malformed.');
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
export function assertIdentity(actual, expected) {
|
|
49
|
+
if (stable(identity(actual)) !== stable(identity(expected)))
|
|
50
|
+
fail('binding', 'Program, binary or connection identity does not match.');
|
|
51
|
+
}
|
|
52
|
+
export function finding(value) {
|
|
53
|
+
const o = object(value);
|
|
54
|
+
const kind = text(o.kind);
|
|
55
|
+
if (!['eol', 'pre', 'post', 'plate', 'repeatable'].includes(kind))
|
|
56
|
+
fail('invalid', 'Only scoped comment operations are supported.');
|
|
57
|
+
const address = text(o.address).toLowerCase();
|
|
58
|
+
if (!/^(?:[a-z0-9_]+:)?[a-f0-9]+$/.test(address))
|
|
59
|
+
fail('invalid', 'An explicit hexadecimal address is required.');
|
|
60
|
+
const comment = (v) => v === null ? null : typeof v === 'string' && v.length <= 16000 && !v.includes('\0') ? v : fail('invalid', 'Comment must be a string or null.');
|
|
61
|
+
return {
|
|
62
|
+
requestId: id(o.requestId), findingId: id(o.findingId), targetId: id(o.targetId),
|
|
63
|
+
identity: identity(o.identity), workerId: id(o.workerId), baselineVersion: version(o.baselineVersion),
|
|
64
|
+
address, kind, before: comment(o.before), after: text(o.after, 16000), evidence: text(o.evidence, 4000),
|
|
65
|
+
...(o.resolves === undefined ? {} : { resolves: id(o.resolves) }),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ResearchBackend } from './research.js';
|
|
2
|
+
import type { Artifact } from './artifacts.js';
|
|
3
|
+
/** Effect assertions run against an immutable saved program, not the response's success flag. */
|
|
4
|
+
export declare function verifyMutation(backend: ResearchBackend, snapshot: Artifact, path: string, p: Record<string, unknown>, response: Record<string, unknown>): Promise<Record<string, unknown>>;
|