rea-agents 0.2.1
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/LICENSE +21 -0
- package/README.md +312 -0
- package/bridge/hopper_bridge.py +557 -0
- package/dist/application/AnalysisProvider.js +1 -0
- package/dist/application/BinarySession.js +173 -0
- package/dist/application/DirectAnalysis.js +31 -0
- package/dist/application/Doctor.js +110 -0
- package/dist/application/EnhancedTools.js +357 -0
- package/dist/application/EvidenceLedger.js +65 -0
- package/dist/application/HopperToolPort.js +1 -0
- package/dist/application/LoopbackReplay.js +84 -0
- package/dist/application/ProcessHarness.js +347 -0
- package/dist/application/Setup.js +240 -0
- package/dist/application/homebrew.js +19 -0
- package/dist/application/runtime.js +11 -0
- package/dist/cli.js +88 -0
- package/dist/config.js +86 -0
- package/dist/contracts/enhancedInputs.js +73 -0
- package/dist/contracts/toolContracts.js +144 -0
- package/dist/contracts/toolOutputSchemas.js +267 -0
- package/dist/domain/binaryTarget.js +321 -0
- package/dist/domain/errors.js +81 -0
- package/dist/domain/evidence.js +146 -0
- package/dist/domain/evidenceBundle.js +11 -0
- package/dist/domain/hopperValues.js +253 -0
- package/dist/domain/jsonValue.js +3 -0
- package/dist/domain/processCapture.js +252 -0
- package/dist/domain/result.js +4 -0
- package/dist/domain/symbolAnalysis.js +68 -0
- package/dist/hopper/BridgeLauncher.js +130 -0
- package/dist/hopper/HopperClient.js +314 -0
- package/dist/hopper/HopperProvider.js +66 -0
- package/dist/hopper/protocol.js +32 -0
- package/dist/identity.js +14 -0
- package/dist/logger.js +27 -0
- package/dist/main.js +81 -0
- package/dist/server/createServer.js +30 -0
- package/dist/server/registerEnhancedTools.js +53 -0
- package/dist/server/registerOfficialTools.js +59 -0
- package/dist/server/registerSessionTools.js +159 -0
- package/dist/server/toolLogging.js +11 -0
- package/dist/server/toolResult.js +44 -0
- package/package.json +109 -0
- package/scripts/rea.mjs +15 -0
- package/scripts/rebuild-native.mjs +22 -0
- package/skills/rea-analysis/SKILL.md +44 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AnalysisProtocolError, HopperProtocolError } from "./errors.js";
|
|
3
|
+
import { err, ok } from "./result.js";
|
|
4
|
+
const procedureMapSchema = z.record(z.string(), z.string());
|
|
5
|
+
const addressedNamesSchema = z.array(z.object({ address: z.string(), name: z.string() }));
|
|
6
|
+
const addressedNameMapSchema = z.record(z.string(), z.string());
|
|
7
|
+
const segmentSchema = z.object({
|
|
8
|
+
name: z.string().default(""),
|
|
9
|
+
start: z.string().default(""),
|
|
10
|
+
end: z.string().default(""),
|
|
11
|
+
});
|
|
12
|
+
const addressedPageSchema = z.object({
|
|
13
|
+
items: z.array(z.object({ address: z.string(), value: z.string() })),
|
|
14
|
+
next_offset: z.number().int().min(0).nullable(),
|
|
15
|
+
has_more: z.boolean(),
|
|
16
|
+
});
|
|
17
|
+
const unavailableSchema = z
|
|
18
|
+
.object({ available: z.literal(false), reason: z.string() })
|
|
19
|
+
.strict();
|
|
20
|
+
const procedureIdentitySchema = z
|
|
21
|
+
.object({ address: z.string(), name: z.string() })
|
|
22
|
+
.strict();
|
|
23
|
+
const boundedSchema = (item) => z
|
|
24
|
+
.object({
|
|
25
|
+
items: z.array(item),
|
|
26
|
+
total: z.number().int().min(0).nullable(),
|
|
27
|
+
returned: z.number().int().min(0),
|
|
28
|
+
truncated: z.boolean(),
|
|
29
|
+
next_offset: z.number().int().min(0).nullable(),
|
|
30
|
+
})
|
|
31
|
+
.strict()
|
|
32
|
+
.superRefine((value, context) => {
|
|
33
|
+
if (value.returned !== value.items.length) {
|
|
34
|
+
context.addIssue({
|
|
35
|
+
code: "custom",
|
|
36
|
+
message: "returned must equal the number of items",
|
|
37
|
+
path: ["returned"],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (value.total !== null && value.total < value.returned) {
|
|
41
|
+
context.addIssue({
|
|
42
|
+
code: "custom",
|
|
43
|
+
message: "total cannot be smaller than returned",
|
|
44
|
+
path: ["total"],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (value.next_offset !== null && !value.truncated) {
|
|
48
|
+
context.addIssue({
|
|
49
|
+
code: "custom",
|
|
50
|
+
message: "a continuation requires truncated output",
|
|
51
|
+
path: ["next_offset"],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const referenceEdgeSchema = z
|
|
56
|
+
.object({
|
|
57
|
+
source_address: z.string(),
|
|
58
|
+
target_address: z.string(),
|
|
59
|
+
source_procedure: procedureIdentitySchema.nullable(),
|
|
60
|
+
target_procedure: procedureIdentitySchema.nullable(),
|
|
61
|
+
kind: unavailableSchema,
|
|
62
|
+
})
|
|
63
|
+
.strict();
|
|
64
|
+
const functionDossierSchema = z
|
|
65
|
+
.object({
|
|
66
|
+
procedure: z
|
|
67
|
+
.object({
|
|
68
|
+
address: z.string(),
|
|
69
|
+
name: z.string(),
|
|
70
|
+
signature: z.string().nullable(),
|
|
71
|
+
locals: z.array(z.json()),
|
|
72
|
+
})
|
|
73
|
+
.strict(),
|
|
74
|
+
pseudocode: z
|
|
75
|
+
.object({
|
|
76
|
+
text: z.string(),
|
|
77
|
+
total_chars: z.number().int().min(0),
|
|
78
|
+
returned_chars: z.number().int().min(0),
|
|
79
|
+
truncated: z.boolean(),
|
|
80
|
+
next_offset: z.number().int().min(0).nullable(),
|
|
81
|
+
})
|
|
82
|
+
.strict()
|
|
83
|
+
.superRefine((value, context) => {
|
|
84
|
+
if (value.returned_chars !== value.text.length) {
|
|
85
|
+
context.addIssue({
|
|
86
|
+
code: "custom",
|
|
87
|
+
message: "returned_chars must equal the text length",
|
|
88
|
+
path: ["returned_chars"],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (value.total_chars < value.returned_chars) {
|
|
92
|
+
context.addIssue({
|
|
93
|
+
code: "custom",
|
|
94
|
+
message: "total_chars cannot be smaller than returned_chars",
|
|
95
|
+
path: ["total_chars"],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (value.next_offset !== null && !value.truncated) {
|
|
99
|
+
context.addIssue({
|
|
100
|
+
code: "custom",
|
|
101
|
+
message: "a continuation requires truncated pseudocode",
|
|
102
|
+
path: ["next_offset"],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}),
|
|
106
|
+
assembly: boundedSchema(z.string()),
|
|
107
|
+
comments: boundedSchema(z
|
|
108
|
+
.object({
|
|
109
|
+
address: z.string(),
|
|
110
|
+
kind: z.enum(["comment", "inline"]),
|
|
111
|
+
text: z.string(),
|
|
112
|
+
})
|
|
113
|
+
.strict()),
|
|
114
|
+
callers: boundedSchema(procedureIdentitySchema),
|
|
115
|
+
callees: boundedSchema(procedureIdentitySchema),
|
|
116
|
+
incoming_references: boundedSchema(referenceEdgeSchema),
|
|
117
|
+
outgoing_references: boundedSchema(referenceEdgeSchema),
|
|
118
|
+
referenced_strings: boundedSchema(z
|
|
119
|
+
.object({
|
|
120
|
+
address: z.string(),
|
|
121
|
+
value: z.string(),
|
|
122
|
+
source_address: z.string(),
|
|
123
|
+
})
|
|
124
|
+
.strict()),
|
|
125
|
+
referenced_names: boundedSchema(z
|
|
126
|
+
.object({
|
|
127
|
+
address: z.string(),
|
|
128
|
+
value: z.string(),
|
|
129
|
+
source_address: z.string(),
|
|
130
|
+
})
|
|
131
|
+
.strict()),
|
|
132
|
+
basic_blocks: boundedSchema(z
|
|
133
|
+
.object({
|
|
134
|
+
start: z.string(),
|
|
135
|
+
end: z.string(),
|
|
136
|
+
successors: z.array(z.string()),
|
|
137
|
+
})
|
|
138
|
+
.strict()),
|
|
139
|
+
instruction_scan: z
|
|
140
|
+
.object({ scanned: z.number().int().min(0), truncated: z.boolean() })
|
|
141
|
+
.strict(),
|
|
142
|
+
})
|
|
143
|
+
.strict();
|
|
144
|
+
/** Strictly parse a complete Hopper function dossier at the provider boundary. */
|
|
145
|
+
export const parseFunctionDossier = (value) => {
|
|
146
|
+
const parsed = functionDossierSchema.safeParse(value);
|
|
147
|
+
return parsed.success
|
|
148
|
+
? ok(parsed.data)
|
|
149
|
+
: err(new AnalysisProtocolError("Invalid analyze_function provider output", {
|
|
150
|
+
cause: parsed.error,
|
|
151
|
+
}));
|
|
152
|
+
};
|
|
153
|
+
/** Parse page entries and continuation metadata returned by list operations. */
|
|
154
|
+
export const parseAddressedPage = (value) => {
|
|
155
|
+
const parsed = addressedPageSchema.safeParse(value);
|
|
156
|
+
return parsed.success
|
|
157
|
+
? ok({
|
|
158
|
+
items: parsed.data.items.map(({ address, value: name }) => ({
|
|
159
|
+
address,
|
|
160
|
+
name,
|
|
161
|
+
})),
|
|
162
|
+
nextOffset: parsed.data.next_offset,
|
|
163
|
+
hasMore: parsed.data.has_more,
|
|
164
|
+
})
|
|
165
|
+
: invalid("addressed page", parsed.error);
|
|
166
|
+
};
|
|
167
|
+
/** Parse Hopper's direct or wrapped procedure map into stable entries. */
|
|
168
|
+
export const parseProcedures = (value) => {
|
|
169
|
+
const page = parseAddressedPage(value);
|
|
170
|
+
if (page.ok)
|
|
171
|
+
return ok(page.value.items);
|
|
172
|
+
const parsed = procedureMapSchema.safeParse(unwrapProperty(value, "procedures"));
|
|
173
|
+
return parsed.success
|
|
174
|
+
? ok(Object.entries(parsed.data).map(([address, name]) => ({
|
|
175
|
+
address,
|
|
176
|
+
name,
|
|
177
|
+
})))
|
|
178
|
+
: page;
|
|
179
|
+
};
|
|
180
|
+
/** Parse Hopper's direct or wrapped list of address/name records. */
|
|
181
|
+
export const parseNames = (value) => {
|
|
182
|
+
const unwrapped = unwrapProperty(value, "names");
|
|
183
|
+
const page = z
|
|
184
|
+
.object({
|
|
185
|
+
items: z.array(z.object({ address: z.string(), value: z.string() })),
|
|
186
|
+
})
|
|
187
|
+
.safeParse(unwrapped);
|
|
188
|
+
if (page.success)
|
|
189
|
+
return ok(page.data.items.map(({ address, value: name }) => ({ address, name })));
|
|
190
|
+
const records = addressedNamesSchema.safeParse(unwrapped);
|
|
191
|
+
if (records.success)
|
|
192
|
+
return ok(records.data);
|
|
193
|
+
const map = addressedNameMapSchema.safeParse(unwrapped);
|
|
194
|
+
return map.success
|
|
195
|
+
? ok(Object.entries(map.data).map(([address, name]) => ({ address, name })))
|
|
196
|
+
: invalid("name list", map.error);
|
|
197
|
+
};
|
|
198
|
+
/** Parse callee/caller strings from direct or wrapped Hopper results. */
|
|
199
|
+
export const parseRelatedAddresses = (value, relation) => {
|
|
200
|
+
const parsed = z.array(z.string()).safeParse(unwrapProperty(value, relation));
|
|
201
|
+
return parsed.success
|
|
202
|
+
? ok(parsed.data)
|
|
203
|
+
: invalid(`${relation} list`, parsed.error);
|
|
204
|
+
};
|
|
205
|
+
/** Parse direct or wrapped Hopper segment records. */
|
|
206
|
+
export const parseSegments = (value) => {
|
|
207
|
+
const parsed = z
|
|
208
|
+
.array(segmentSchema)
|
|
209
|
+
.safeParse(unwrapProperty(value, "segments"));
|
|
210
|
+
return parsed.success
|
|
211
|
+
? ok(parsed.data)
|
|
212
|
+
: invalid("segment list", parsed.error);
|
|
213
|
+
};
|
|
214
|
+
/** Parse direct or wrapped Hopper document names. */
|
|
215
|
+
export const parseDocuments = (value) => {
|
|
216
|
+
const parsed = z
|
|
217
|
+
.array(z.string())
|
|
218
|
+
.safeParse(unwrapProperty(value, "documents"));
|
|
219
|
+
return parsed.success
|
|
220
|
+
? ok(parsed.data)
|
|
221
|
+
: invalid("document list", parsed.error);
|
|
222
|
+
};
|
|
223
|
+
/** Parse a direct or wrapped list when only its cardinality is required. */
|
|
224
|
+
export const parseListCount = (value, property) => {
|
|
225
|
+
const unwrapped = unwrapProperty(value, property);
|
|
226
|
+
const pageTotal = z
|
|
227
|
+
.object({ total: z.number().int().min(0) })
|
|
228
|
+
.safeParse(unwrapped);
|
|
229
|
+
if (pageTotal.success)
|
|
230
|
+
return ok(pageTotal.data.total);
|
|
231
|
+
const page = z.object({ items: z.array(z.unknown()) }).safeParse(unwrapped);
|
|
232
|
+
if (page.success)
|
|
233
|
+
return ok(page.data.items.length);
|
|
234
|
+
const list = z.array(z.unknown()).safeParse(unwrapped);
|
|
235
|
+
if (list.success)
|
|
236
|
+
return ok(list.data.length);
|
|
237
|
+
const map = z.record(z.string(), z.unknown()).safeParse(unwrapped);
|
|
238
|
+
return map.success
|
|
239
|
+
? ok(Object.keys(map.data).length)
|
|
240
|
+
: invalid(`${property} list`, map.error);
|
|
241
|
+
};
|
|
242
|
+
const unwrapProperty = (value, property) => {
|
|
243
|
+
if (typeof value === "object" &&
|
|
244
|
+
value !== null &&
|
|
245
|
+
!Array.isArray(value) &&
|
|
246
|
+
property in value) {
|
|
247
|
+
return value[property] ?? null;
|
|
248
|
+
}
|
|
249
|
+
return value;
|
|
250
|
+
};
|
|
251
|
+
const invalid = (expected, cause) => err(new HopperProtocolError(`Hopper returned an invalid ${expected}`, {
|
|
252
|
+
cause,
|
|
253
|
+
}));
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const positiveBudget = z.number().int().positive();
|
|
4
|
+
const timedEventBase = { at_ms: z.number().int().nonnegative() };
|
|
5
|
+
/** Exact boundary schema for bounded dynamic process scenarios. */
|
|
6
|
+
export const processScenarioSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
approved: z
|
|
9
|
+
.literal(true)
|
|
10
|
+
.describe("Explicit per-call acknowledgement that this operation launches the target"),
|
|
11
|
+
executable: z.string().startsWith("/"),
|
|
12
|
+
arguments: z.array(z.string()).max(256).default([]),
|
|
13
|
+
working_directory: z.string().startsWith("/"),
|
|
14
|
+
environment: z.record(z.string(), z.string()).default({}),
|
|
15
|
+
inherit_environment: z.array(z.string()).max(64).default([]),
|
|
16
|
+
secret_aliases: z.array(z.string()).max(64).default([]),
|
|
17
|
+
filesystem_roots: z.array(z.string().startsWith("/")).max(16).default([]),
|
|
18
|
+
events: z
|
|
19
|
+
.array(z.discriminatedUnion("type", [
|
|
20
|
+
z.object({
|
|
21
|
+
...timedEventBase,
|
|
22
|
+
type: z.literal("input"),
|
|
23
|
+
data: z.string(),
|
|
24
|
+
}),
|
|
25
|
+
z.object({
|
|
26
|
+
...timedEventBase,
|
|
27
|
+
type: z.literal("resize"),
|
|
28
|
+
columns: z.number().int().min(1).max(1_000),
|
|
29
|
+
rows: z.number().int().min(1).max(1_000),
|
|
30
|
+
}),
|
|
31
|
+
z.object({
|
|
32
|
+
...timedEventBase,
|
|
33
|
+
type: z.literal("signal"),
|
|
34
|
+
signal: z.enum(["SIGINT", "SIGTERM", "SIGKILL"]),
|
|
35
|
+
}),
|
|
36
|
+
]))
|
|
37
|
+
.max(1_000)
|
|
38
|
+
.default([]),
|
|
39
|
+
timeout_ms: positiveBudget.max(300_000).default(30_000),
|
|
40
|
+
idle_timeout_ms: positiveBudget.max(300_000).default(30_000),
|
|
41
|
+
settle_ms: z.number().int().nonnegative().max(10_000).default(100),
|
|
42
|
+
limits: z
|
|
43
|
+
.object({
|
|
44
|
+
output_bytes: positiveBudget.max(10_000_000).default(1_000_000),
|
|
45
|
+
frames: positiveBudget.max(100_000).default(10_000),
|
|
46
|
+
files: positiveBudget.max(100_000).default(10_000),
|
|
47
|
+
file_bytes: positiveBudget.max(100_000_000).default(10_000_000),
|
|
48
|
+
processes: positiveBudget.max(10_000).default(1_000),
|
|
49
|
+
})
|
|
50
|
+
.default({
|
|
51
|
+
output_bytes: 1_000_000,
|
|
52
|
+
frames: 10_000,
|
|
53
|
+
files: 10_000,
|
|
54
|
+
file_bytes: 10_000_000,
|
|
55
|
+
processes: 1_000,
|
|
56
|
+
}),
|
|
57
|
+
normalization: z
|
|
58
|
+
.object({
|
|
59
|
+
paths: z.boolean().default(true),
|
|
60
|
+
pids: z.boolean().default(true),
|
|
61
|
+
ports: z.boolean().default(true),
|
|
62
|
+
time_bucket_ms: positiveBudget.max(60_000).default(10),
|
|
63
|
+
patterns: z
|
|
64
|
+
.array(z.object({
|
|
65
|
+
pattern: z.string().max(500),
|
|
66
|
+
replacement: z.string().max(100),
|
|
67
|
+
}))
|
|
68
|
+
.max(32)
|
|
69
|
+
.default([]),
|
|
70
|
+
})
|
|
71
|
+
.default({
|
|
72
|
+
paths: true,
|
|
73
|
+
pids: true,
|
|
74
|
+
ports: true,
|
|
75
|
+
time_bucket_ms: 10,
|
|
76
|
+
patterns: [],
|
|
77
|
+
}),
|
|
78
|
+
replay: z
|
|
79
|
+
.object({
|
|
80
|
+
http: z
|
|
81
|
+
.array(z.object({
|
|
82
|
+
method: z.string().max(16),
|
|
83
|
+
path: z.string().startsWith("/"),
|
|
84
|
+
status: z.number().int().min(100).max(599),
|
|
85
|
+
body: z.string().max(1_000_000),
|
|
86
|
+
}))
|
|
87
|
+
.max(100)
|
|
88
|
+
.default([]),
|
|
89
|
+
websocket_messages: z
|
|
90
|
+
.array(z.string().max(1_000_000))
|
|
91
|
+
.max(100)
|
|
92
|
+
.default([]),
|
|
93
|
+
})
|
|
94
|
+
.default({ http: [], websocket_messages: [] }),
|
|
95
|
+
})
|
|
96
|
+
.strict()
|
|
97
|
+
.superRefine((scenario, context) => {
|
|
98
|
+
for (let index = 1; index < scenario.events.length; index += 1) {
|
|
99
|
+
if (scenario.events[index].at_ms < scenario.events[index - 1].at_ms) {
|
|
100
|
+
context.addIssue({
|
|
101
|
+
code: "custom",
|
|
102
|
+
message: "events must be ordered by at_ms",
|
|
103
|
+
path: ["events", index],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const secrets = new Set(scenario.secret_aliases);
|
|
108
|
+
for (const alias of secrets) {
|
|
109
|
+
if (!(alias in scenario.environment)) {
|
|
110
|
+
context.addIssue({
|
|
111
|
+
code: "custom",
|
|
112
|
+
message: "secret alias has no environment value",
|
|
113
|
+
path: ["secret_aliases"],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
/** Parse untrusted process scenario input and apply safe default budgets. */
|
|
119
|
+
export const parseProcessScenario = (input) => processScenarioSchema.parse(input);
|
|
120
|
+
const isWithin = (candidate, root) => resolve(candidate) === resolve(root) ||
|
|
121
|
+
resolve(candidate).startsWith(`${resolve(root)}${resolve(root).endsWith("/") ? "" : "/"}`);
|
|
122
|
+
/** Evaluate scenario authority before any process or filesystem side effect occurs. */
|
|
123
|
+
export const authorizeProcessScenario = (scenario, policy) => {
|
|
124
|
+
if (!policy.enabled)
|
|
125
|
+
return { allowed: false, reason: "process capture is disabled" };
|
|
126
|
+
if (!policy.executableRoots.some((root) => isWithin(scenario.executable, root))) {
|
|
127
|
+
return { allowed: false, reason: "executable is outside approved roots" };
|
|
128
|
+
}
|
|
129
|
+
if (!policy.workingRoots.some((root) => isWithin(scenario.working_directory, root))) {
|
|
130
|
+
return {
|
|
131
|
+
allowed: false,
|
|
132
|
+
reason: "working directory is outside approved roots",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const requestedNames = [
|
|
136
|
+
...Object.keys(scenario.environment),
|
|
137
|
+
...scenario.inherit_environment,
|
|
138
|
+
];
|
|
139
|
+
if (requestedNames.some((name) => !policy.allowedEnvironment.includes(name))) {
|
|
140
|
+
return {
|
|
141
|
+
allowed: false,
|
|
142
|
+
reason: "scenario requests an environment variable not allowed by policy",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (scenario.filesystem_roots.some((path) => !policy.workingRoots.some((root) => isWithin(path, root)))) {
|
|
146
|
+
return {
|
|
147
|
+
allowed: false,
|
|
148
|
+
reason: "filesystem root is outside approved roots",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return { allowed: true };
|
|
152
|
+
};
|
|
153
|
+
/** Exact serialized shape of a bounded process capture. */
|
|
154
|
+
export const processCaptureSchema = z.object({
|
|
155
|
+
schema_version: z.literal(1),
|
|
156
|
+
frames: z.array(z.object({
|
|
157
|
+
sequence: z.number().int().nonnegative(),
|
|
158
|
+
at_ms: z.number().int().nonnegative(),
|
|
159
|
+
data: z.string(),
|
|
160
|
+
})),
|
|
161
|
+
exit: z.object({
|
|
162
|
+
code: z.number().int().nullable(),
|
|
163
|
+
signal: z.number().int().nullable(),
|
|
164
|
+
}),
|
|
165
|
+
process_samples: z.array(z.object({
|
|
166
|
+
at_ms: z.number().int().nonnegative(),
|
|
167
|
+
pid: z.number().int().positive(),
|
|
168
|
+
parent_pid: z.number().int().nonnegative(),
|
|
169
|
+
command: z.string(),
|
|
170
|
+
})),
|
|
171
|
+
protocol_events: z.array(z.object({
|
|
172
|
+
sequence: z.number().int().nonnegative(),
|
|
173
|
+
protocol: z.enum(["http", "websocket"]),
|
|
174
|
+
direction: z.enum(["request", "response", "received", "sent"]),
|
|
175
|
+
method: z.string().nullable(),
|
|
176
|
+
path: z.string().nullable(),
|
|
177
|
+
data: z.string(),
|
|
178
|
+
})),
|
|
179
|
+
files_before: z.array(z.object({
|
|
180
|
+
path: z.string(),
|
|
181
|
+
type: z.enum(["file", "directory", "symlink", "other"]),
|
|
182
|
+
mode: z.number().int().nonnegative(),
|
|
183
|
+
size: z.number().int().nonnegative(),
|
|
184
|
+
sha256: z
|
|
185
|
+
.string()
|
|
186
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
187
|
+
.nullable(),
|
|
188
|
+
symlink_target: z.string().nullable(),
|
|
189
|
+
})),
|
|
190
|
+
files_after: z.array(z.object({
|
|
191
|
+
path: z.string(),
|
|
192
|
+
type: z.enum(["file", "directory", "symlink", "other"]),
|
|
193
|
+
mode: z.number().int().nonnegative(),
|
|
194
|
+
size: z.number().int().nonnegative(),
|
|
195
|
+
sha256: z
|
|
196
|
+
.string()
|
|
197
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
198
|
+
.nullable(),
|
|
199
|
+
symlink_target: z.string().nullable(),
|
|
200
|
+
})),
|
|
201
|
+
truncated: z.boolean(),
|
|
202
|
+
limitations: z.array(z.string()),
|
|
203
|
+
});
|
|
204
|
+
const stableFiles = (files) => JSON.stringify([...files].sort((left, right) => left.path.localeCompare(right.path)));
|
|
205
|
+
/** Compare bounded captures without claiming equality for incomplete observations. */
|
|
206
|
+
export const compareProcessCaptures = (left, right) => {
|
|
207
|
+
if (left.truncated || right.truncated) {
|
|
208
|
+
return {
|
|
209
|
+
status: "truncated",
|
|
210
|
+
terminal: "truncated",
|
|
211
|
+
exit: "truncated",
|
|
212
|
+
filesystem: "truncated",
|
|
213
|
+
protocol: "truncated",
|
|
214
|
+
process: "truncated",
|
|
215
|
+
limitations: ["At least one capture is truncated."],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const terminal = left.frames.map(({ data }) => data).join("") ===
|
|
219
|
+
right.frames.map(({ data }) => data).join("")
|
|
220
|
+
? "unchanged"
|
|
221
|
+
: "changed";
|
|
222
|
+
const exit = JSON.stringify(left.exit) === JSON.stringify(right.exit)
|
|
223
|
+
? "unchanged"
|
|
224
|
+
: "changed";
|
|
225
|
+
const filesystem = stableFiles(left.files_after) === stableFiles(right.files_after)
|
|
226
|
+
? "unchanged"
|
|
227
|
+
: "changed";
|
|
228
|
+
const protocol = JSON.stringify(left.protocol_events) ===
|
|
229
|
+
JSON.stringify(right.protocol_events)
|
|
230
|
+
? "unchanged"
|
|
231
|
+
: "changed";
|
|
232
|
+
const process = JSON.stringify(left.process_samples.map(({ command }) => command)) ===
|
|
233
|
+
JSON.stringify(right.process_samples.map(({ command }) => command))
|
|
234
|
+
? "unchanged"
|
|
235
|
+
: "changed";
|
|
236
|
+
const status = terminal === "unchanged" &&
|
|
237
|
+
exit === "unchanged" &&
|
|
238
|
+
filesystem === "unchanged" &&
|
|
239
|
+
protocol === "unchanged" &&
|
|
240
|
+
process === "unchanged"
|
|
241
|
+
? "unchanged"
|
|
242
|
+
: "changed";
|
|
243
|
+
return {
|
|
244
|
+
status,
|
|
245
|
+
terminal,
|
|
246
|
+
exit,
|
|
247
|
+
filesystem,
|
|
248
|
+
protocol,
|
|
249
|
+
process,
|
|
250
|
+
limitations: [...left.limitations, ...right.limitations],
|
|
251
|
+
};
|
|
252
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const MAX_SCANNED_SYMBOLS = 5_000;
|
|
2
|
+
const MAX_RETURNED_SYMBOLS = 100;
|
|
3
|
+
/** Select Swift class symbols using the legacy `_TtC` convention. */
|
|
4
|
+
export const discoverSwiftClasses = (procedures, pattern) => {
|
|
5
|
+
const classes = procedures
|
|
6
|
+
.slice(0, MAX_SCANNED_SYMBOLS)
|
|
7
|
+
.filter(({ name }) => name.includes("_TtC"))
|
|
8
|
+
.filter(({ name }) => pattern.length === 0 || name.includes(pattern));
|
|
9
|
+
return {
|
|
10
|
+
count: classes.length,
|
|
11
|
+
classes: classes.slice(0, MAX_RETURNED_SYMBOLS).map(toJsonEntry),
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/** Select and deduplicate Objective-C class labels. */
|
|
15
|
+
export const discoverObjcClasses = (names, pattern) => {
|
|
16
|
+
const classes = uniqueByName(names
|
|
17
|
+
.filter(({ name }) => ["OBJC_CLASS", "OBJC_$_CLASS"].some((marker) => name.includes(marker)) || name.startsWith("_OBJC_"))
|
|
18
|
+
.filter(({ name }) => pattern.length === 0 || name.includes(pattern)));
|
|
19
|
+
return {
|
|
20
|
+
count: classes.length,
|
|
21
|
+
classes: classes.slice(0, MAX_RETURNED_SYMBOLS).map(toJsonEntry),
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
/** Select and deduplicate Objective-C and Swift protocol labels. */
|
|
25
|
+
export const discoverObjcProtocols = (names) => {
|
|
26
|
+
const protocols = uniqueByName(names.filter(({ name }) => name.includes("OBJC_PROTOCOL") || name.includes("_TtP")));
|
|
27
|
+
return {
|
|
28
|
+
count: protocols.length,
|
|
29
|
+
protocols: protocols.slice(0, MAX_RETURNED_SYMBOLS).map(toJsonEntry),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const SWIFT_CATEGORIES = [
|
|
33
|
+
["classes", "_TtC"],
|
|
34
|
+
["structs", "_TtV"],
|
|
35
|
+
["enums", "_TtO"],
|
|
36
|
+
["protocols", "_TtP"],
|
|
37
|
+
["extensions", "_TtE"],
|
|
38
|
+
];
|
|
39
|
+
/** Categorize deduplicated Swift mangled symbols. */
|
|
40
|
+
export const categorizeSwiftTypes = (procedures) => {
|
|
41
|
+
const groups = Object.fromEntries([...SWIFT_CATEGORIES.map(([category]) => category), "other"].map((category) => [category, []]));
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
for (const entry of procedures.slice(0, MAX_SCANNED_SYMBOLS)) {
|
|
44
|
+
if (!entry.name.includes("_Tt") || seen.has(entry.name))
|
|
45
|
+
continue;
|
|
46
|
+
seen.add(entry.name);
|
|
47
|
+
const category = SWIFT_CATEGORIES.find(([, prefix]) => entry.name.startsWith(prefix))?.[0] ?? "other";
|
|
48
|
+
groups[category]?.push(entry);
|
|
49
|
+
}
|
|
50
|
+
const categories = Object.fromEntries(Object.entries(groups).map(([category, items]) => [
|
|
51
|
+
category,
|
|
52
|
+
{ count: items.length, items: items.slice(0, 50).map(toJsonEntry) },
|
|
53
|
+
]));
|
|
54
|
+
return { total: seen.size, categories };
|
|
55
|
+
};
|
|
56
|
+
const uniqueByName = (entries) => {
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
return entries.filter(({ name }) => {
|
|
59
|
+
if (seen.has(name))
|
|
60
|
+
return false;
|
|
61
|
+
seen.add(name);
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
const toJsonEntry = ({ address, name }) => ({
|
|
66
|
+
address,
|
|
67
|
+
name,
|
|
68
|
+
});
|