@mobileaidev/ai-app-bridge 0.2.15 → 0.3.0-rc.2
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 +201 -0
- package/NOTICE +7 -0
- package/README.md +306 -53
- package/bin/ai-app-bridge.js +51 -4504
- package/bin/android-permissions.js +152 -0
- package/bin/android-uia-xml.js +74 -0
- package/bin/artifact-paths.js +127 -1
- package/bin/bridge-forward.js +56 -0
- package/bin/command-discovery.js +86 -0
- package/bin/command-errors.js +60 -0
- package/bin/command-registry.js +504 -0
- package/bin/command-request.js +14 -0
- package/bin/command-router.js +80 -0
- package/bin/connection-cache.js +119 -0
- package/bin/device-provider.js +2934 -0
- package/bin/execution-host.js +596 -0
- package/bin/execution-runtime.js +101 -0
- package/bin/fact-codec.js +321 -0
- package/bin/fact-recorder.js +691 -0
- package/bin/fact-store.js +226 -0
- package/bin/feedback-probe.js +285 -0
- package/bin/intent/install-intent.js +267 -0
- package/bin/intent/intent-action-executor.js +129 -0
- package/bin/intent/intent-autonomous-adapter.js +46 -0
- package/bin/intent/intent-capture-port.js +64 -0
- package/bin/intent/intent-entry.js +226 -0
- package/bin/intent/intent-errors.js +28 -0
- package/bin/intent/intent-evidence-store.js +121 -0
- package/bin/intent/intent-lifetime.js +62 -0
- package/bin/intent/intent-observation-target.js +33 -0
- package/bin/intent/intent-observer.js +194 -0
- package/bin/intent/intent-production-adapter.js +334 -0
- package/bin/intent/intent-provider.js +27 -0
- package/bin/intent/intent-runtime.js +63 -0
- package/bin/intent/intent-worker.js +432 -0
- package/bin/intent/ios-intent-adapter.js +90 -0
- package/bin/intent/permission-intent.js +239 -0
- package/bin/intent/web-intent-adapter.js +31 -0
- package/bin/ios-device-outcome.js +47 -0
- package/bin/ios-execution.js +108 -0
- package/bin/ios-provider.js +497 -583
- package/bin/ios-runtime-binding.js +54 -0
- package/bin/ios-wda-execution.js +70 -0
- package/bin/ios-wda-port.js +98 -0
- package/bin/ios-wda-project.js +79 -0
- package/bin/mcp-server.js +83 -1040
- package/bin/mmap-scan-index.js +329 -0
- package/bin/observation-collector.js +862 -0
- package/bin/runtime-client.js +154 -0
- package/bin/runtime-directory.js +107 -0
- package/bin/runtime-protocol.js +36 -0
- package/bin/script/bounded-script-registry.js +103 -0
- package/bin/script/node-runtime-adapter.js +233 -0
- package/bin/script/progress-projector.js +63 -0
- package/bin/script/python-runtime-adapter.js +111 -0
- package/bin/script/rolling-summary.js +134 -0
- package/bin/script/script-agent-port.js +40 -0
- package/bin/script/script-assert.js +95 -0
- package/bin/script/script-capture-port.js +76 -0
- package/bin/script/script-catalog.js +85 -0
- package/bin/script/script-durable-restore.js +195 -0
- package/bin/script/script-entry-code.js +26 -0
- package/bin/script/script-entry-route.js +38 -0
- package/bin/script/script-entry.js +3 -0
- package/bin/script/script-errors.js +29 -0
- package/bin/script/script-evidence-store.js +22 -0
- package/bin/script/script-format-removed.js +26 -0
- package/bin/script/script-host-port.js +397 -0
- package/bin/script/script-ledger.js +64 -0
- package/bin/script/script-result.js +57 -0
- package/bin/script/script-sdk.js +152 -0
- package/bin/script/script-sdk.py +153 -0
- package/bin/script/script-session-channel.js +127 -0
- package/bin/script/script-spec.js +86 -0
- package/bin/script/script-supervisor.js +919 -0
- package/bin/script/templates/checkpoint-reentry.js +13 -0
- package/bin/segment-index.js +481 -0
- package/bin/segmented-fact-store.js +1571 -0
- package/bin/shared-kernel/android-h5-target.js +10 -0
- package/bin/shared-kernel/android-install-execution.js +176 -0
- package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
- package/bin/shared-kernel/android-shell-execution.js +195 -0
- package/bin/shared-kernel/argument-schema.js +117 -0
- package/bin/shared-kernel/canonical-path.js +17 -0
- package/bin/shared-kernel/device-acknowledgements.js +53 -0
- package/bin/shared-kernel/device-completion-history.js +52 -0
- package/bin/shared-kernel/device-mutation-lease.js +219 -0
- package/bin/shared-kernel/device-ownership-recovery.js +95 -0
- package/bin/shared-kernel/device-ownership-store.js +94 -0
- package/bin/shared-kernel/evidence-adapters.js +251 -0
- package/bin/shared-kernel/evidence-archive.js +329 -0
- package/bin/shared-kernel/evidence-recording.js +131 -0
- package/bin/shared-kernel/evidence-schema.js +194 -0
- package/bin/shared-kernel/evidence-store.js +190 -0
- package/bin/shared-kernel/execution-admission.js +22 -0
- package/bin/shared-kernel/execution-contracts.js +171 -0
- package/bin/shared-kernel/execution-io.js +106 -0
- package/bin/shared-kernel/execution-ledger.js +125 -0
- package/bin/shared-kernel/execution-scope.js +87 -0
- package/bin/shared-kernel/execution-target.js +115 -0
- package/bin/shared-kernel/flutter-execution.js +13 -0
- package/bin/shared-kernel/flutter-h5-port.js +60 -0
- package/bin/shared-kernel/flutter-h5-target.js +9 -0
- package/bin/shared-kernel/flutter-target.js +75 -0
- package/bin/shared-kernel/h5-execution.js +11 -0
- package/bin/shared-kernel/h5-target.js +31 -0
- package/bin/shared-kernel/host-fact-store.js +49 -0
- package/bin/shared-kernel/ios-h5-target.js +9 -0
- package/bin/shared-kernel/ios-native-target.js +71 -0
- package/bin/shared-kernel/live-capture-query.js +115 -0
- package/bin/shared-kernel/managed-sdk-execution.js +78 -0
- package/bin/shared-kernel/native-execution.js +13 -0
- package/bin/shared-kernel/native-target.js +156 -0
- package/bin/shared-kernel/provider-command-contracts.js +55 -0
- package/bin/shared-kernel/recorded-payload-archive.js +195 -0
- package/bin/shared-kernel/request-context.js +35 -0
- package/bin/shared-kernel/semantic-node.js +55 -0
- package/bin/shared-kernel/summary-transformer.js +352 -0
- package/bin/shared-kernel/target-lease-protocol.js +47 -0
- package/bin/shared-kernel/text-wait.js +111 -0
- package/bin/shared-kernel/uia-execution.js +96 -0
- package/bin/shared-kernel/uia-protocol.js +214 -0
- package/bin/shared-kernel/uia-runtime-port.js +377 -0
- package/bin/shared-kernel/uia-target.js +39 -0
- package/bin/shared-kernel/web-dom-target.js +44 -0
- package/bin/shared-kernel/xml-attributes.js +25 -0
- package/bin/target-execution.js +275 -0
- package/bin/web/command-schema.js +60 -0
- package/bin/web/session-store.js +157 -0
- package/bin/web-provider.js +334 -553
- package/docs/COMMAND_CONTRACT.md +1563 -0
- package/docs/EVIDENCE_ARCHIVE.md +214 -0
- package/docs/INTENT_FOREGROUND.md +71 -0
- package/docs/INTENT_NATIVE_EDITING.md +79 -0
- package/docs/RELEASE.md +59 -0
- package/docs/SCRIPT_AUTHORING.md +489 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
- package/package.json +60 -5
- package/runtime/ios-wda/AABWDABinding.h +19 -0
- package/runtime/ios-wda/AABWDABinding.m +97 -0
- package/runtime/ios-wda/AABWDAExecution.h +26 -0
- package/runtime/ios-wda/AABWDAExecution.m +172 -0
- package/runtime/ios-wda/AABWDAIntegration.h +71 -0
- package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
- package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
- package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
- package/runtime/uia/ai-app-bridge-uia.jar +0 -0
- package/runtime/uia/manifest.json +22 -0
- package/skills/ai-app-bridge-use/SKILL.md +23 -360
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { targetFor } = require('./target-execution');
|
|
4
|
+
|
|
5
|
+
const UI_EVIDENCE_COMMANDS = new Set([
|
|
6
|
+
'tree',
|
|
7
|
+
'uia-tree',
|
|
8
|
+
'flutter-tree',
|
|
9
|
+
'flutter-nodes',
|
|
10
|
+
'screenshot',
|
|
11
|
+
'h5-dom',
|
|
12
|
+
'web-dom',
|
|
13
|
+
'ios-tree',
|
|
14
|
+
'ios-uia-tree',
|
|
15
|
+
'ios-screenshot',
|
|
16
|
+
'ios-h5-dom',
|
|
17
|
+
'ios-flutter-tree',
|
|
18
|
+
'ios-flutter-nodes',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const REDACTED = '[REDACTED]';
|
|
22
|
+
const SECURE_TEXT_KEYS = new Set([
|
|
23
|
+
'text',
|
|
24
|
+
'value',
|
|
25
|
+
]);
|
|
26
|
+
const SECURE_FLAG_KEYS = new Set([
|
|
27
|
+
'secure',
|
|
28
|
+
'sensitive',
|
|
29
|
+
'obscured',
|
|
30
|
+
'ispassword',
|
|
31
|
+
'issecure',
|
|
32
|
+
'issecuretextentry',
|
|
33
|
+
'securetextentry',
|
|
34
|
+
'isobscured',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
class FactRecorder {
|
|
38
|
+
constructor({ cache, now = Date.now, hostEpoch, actionCompletionGraceMs = 250 } = {}) {
|
|
39
|
+
if (!cache || typeof cache.append !== 'function' || typeof cache.query !== 'function') {
|
|
40
|
+
throw new TypeError('cache with append/query is required');
|
|
41
|
+
}
|
|
42
|
+
if (typeof now !== 'function') throw new TypeError('now must be a function');
|
|
43
|
+
if (!Number.isFinite(actionCompletionGraceMs) || actionCompletionGraceMs < 0) {
|
|
44
|
+
throw new TypeError('actionCompletionGraceMs must be a non-negative number');
|
|
45
|
+
}
|
|
46
|
+
this.cache = cache;
|
|
47
|
+
this.now = now;
|
|
48
|
+
this.actionCompletionGraceMs = actionCompletionGraceMs;
|
|
49
|
+
this.hostEpoch = hostEpoch || `host-${process.pid}-${now()}`;
|
|
50
|
+
this.runtimeEpochs = new Map();
|
|
51
|
+
this.actionSequence = 0;
|
|
52
|
+
this.seenEvidence = new Set();
|
|
53
|
+
this.seenEvidenceOrder = [];
|
|
54
|
+
this.maxSeenEvidence = 20_000;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
recordExecution({ command, args = {}, result, error, feedback, actionId } = {}) {
|
|
58
|
+
const target = targetFor(command, args);
|
|
59
|
+
const resolvedActionId = actionId || args.requestId || this.nextActionId();
|
|
60
|
+
const runtimeEpoch = this.observeRuntimeEpoch(target.key, result, args.runtimeEpoch);
|
|
61
|
+
if (args.requestId) {
|
|
62
|
+
const existing = this.cache.query({
|
|
63
|
+
partition: 'action',
|
|
64
|
+
target: target.key,
|
|
65
|
+
actionId: String(resolvedActionId),
|
|
66
|
+
limit: 1,
|
|
67
|
+
});
|
|
68
|
+
if (existing.ok && existing.items.length > 0) {
|
|
69
|
+
const fact = existing.items[0];
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
stored: true,
|
|
73
|
+
globalSeq: fact.globalSeq,
|
|
74
|
+
sizeBytes: fact.sizeBytes,
|
|
75
|
+
evictedCount: 0,
|
|
76
|
+
deduplicated: true,
|
|
77
|
+
partition: 'action',
|
|
78
|
+
targetKey: target.key,
|
|
79
|
+
runtimeEpoch: fact.runtimeEpoch,
|
|
80
|
+
actionId: String(resolvedActionId),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const observedAtMs = feedback?.timings?.completedAtMs || this.now();
|
|
85
|
+
const appendResult = this.cache.append('action', {
|
|
86
|
+
targetKey: target.key,
|
|
87
|
+
app: appIdentity(target, result),
|
|
88
|
+
runtimeEpoch,
|
|
89
|
+
actionId: resolvedActionId,
|
|
90
|
+
timestamps: {
|
|
91
|
+
occurredAtMs: feedback?.timings?.requestedAtMs || observedAtMs,
|
|
92
|
+
observedAtMs,
|
|
93
|
+
},
|
|
94
|
+
payload: {
|
|
95
|
+
kind: 'execution',
|
|
96
|
+
command,
|
|
97
|
+
status: feedback?.status || executionStatus(result, error),
|
|
98
|
+
args: summarizeArgs(args),
|
|
99
|
+
result: summarizeResult(result),
|
|
100
|
+
...(error ? { error: String(error.message || error).slice(0, 1_000) } : {}),
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
...appendResult,
|
|
105
|
+
partition: 'action',
|
|
106
|
+
targetKey: target.key,
|
|
107
|
+
runtimeEpoch,
|
|
108
|
+
actionId: String(resolvedActionId),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
recordEvidence(command, args = {}, result, context = {}) {
|
|
113
|
+
if (!result || typeof result !== 'object' || Buffer.isBuffer(result)) return [];
|
|
114
|
+
if (isMobileCaptureCommand(command)) return [];
|
|
115
|
+
if (command.startsWith('web-') && (result.source === 'host-fact-store' || result.receipt?.source === 'host-fact-store')) {
|
|
116
|
+
return result.receipt ? [result.receipt] : (result.items || []).map(item => ({ ...item.ref, stored: true }));
|
|
117
|
+
}
|
|
118
|
+
const target = targetFor(command, args);
|
|
119
|
+
const runtimeEpoch = this.observeRuntimeEpoch(
|
|
120
|
+
target.key,
|
|
121
|
+
result,
|
|
122
|
+
context.runtimeEpoch || args.runtimeEpoch,
|
|
123
|
+
);
|
|
124
|
+
const descriptors = evidenceDescriptors(command, result);
|
|
125
|
+
const references = [];
|
|
126
|
+
for (const descriptor of descriptors) {
|
|
127
|
+
const record = descriptor.record;
|
|
128
|
+
const persistedRecord = shouldProjectPrivateUiEvidence(command, descriptor)
|
|
129
|
+
? privacyProjectUiEvidence(record)
|
|
130
|
+
: record;
|
|
131
|
+
const evidenceKey = record?.id === undefined || record?.id === null
|
|
132
|
+
? null
|
|
133
|
+
: JSON.stringify([target.key, runtimeEpoch, descriptor.stream, String(record.id)]);
|
|
134
|
+
if (evidenceKey && this.seenEvidence.has(evidenceKey)) continue;
|
|
135
|
+
const evidenceTimestampMs = optionalTimestamp(
|
|
136
|
+
record?.timestampMs ?? record?.updatedAtMs ?? result.updatedAtMs,
|
|
137
|
+
);
|
|
138
|
+
const observedAtMs = evidenceTimestampMs ?? this.now();
|
|
139
|
+
const appended = this.cache.append(descriptor.partition, {
|
|
140
|
+
targetKey: target.key,
|
|
141
|
+
app: appIdentity(target, result),
|
|
142
|
+
runtimeEpoch,
|
|
143
|
+
actionId: actionIdForEvidence({
|
|
144
|
+
context,
|
|
145
|
+
fallbackActionId: args.requestId,
|
|
146
|
+
runtimeActionId: record?.actionId,
|
|
147
|
+
evidenceTimestampMs,
|
|
148
|
+
completionGraceMs: this.actionCompletionGraceMs,
|
|
149
|
+
}),
|
|
150
|
+
...(evidenceKey ? { dedupeKey: evidenceKey } : {}),
|
|
151
|
+
timestamps: {
|
|
152
|
+
occurredAtMs: observedAtMs,
|
|
153
|
+
observedAtMs,
|
|
154
|
+
},
|
|
155
|
+
payload: {
|
|
156
|
+
kind: 'evidence',
|
|
157
|
+
stream: descriptor.stream,
|
|
158
|
+
record: persistedRecord,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
references.push({
|
|
162
|
+
partition: descriptor.partition,
|
|
163
|
+
globalSeq: appended.globalSeq ?? null,
|
|
164
|
+
stored: appended.ok !== false && appended.stored === true,
|
|
165
|
+
...(appended.ok === false || appended.stored !== true ? { error: appended.error || 'fact_evidence_not_stored' } : {}),
|
|
166
|
+
...(appended.deduplicated ? { deduplicated: true } : {}),
|
|
167
|
+
});
|
|
168
|
+
if (evidenceKey && appended.ok !== false && appended.stored === true) this.rememberEvidence(evidenceKey);
|
|
169
|
+
}
|
|
170
|
+
return references;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
recordDeviceLog(args = {}, batch = {}, context = {}) {
|
|
174
|
+
const command = 'logcat';
|
|
175
|
+
const target = targetFor(command, args);
|
|
176
|
+
const runtimeEpoch = this.observeRuntimeEpoch(
|
|
177
|
+
target.key,
|
|
178
|
+
null,
|
|
179
|
+
context.runtimeEpoch || args.runtimeEpoch,
|
|
180
|
+
);
|
|
181
|
+
const evidenceTimestampMs = optionalTimestamp(batch.observedAtMs);
|
|
182
|
+
const observedAtMs = evidenceTimestampMs ?? this.now();
|
|
183
|
+
const appended = this.cache.append('device-log', {
|
|
184
|
+
targetKey: target.key,
|
|
185
|
+
app: appIdentity(target, null),
|
|
186
|
+
runtimeEpoch,
|
|
187
|
+
actionId: actionIdForEvidence({
|
|
188
|
+
context,
|
|
189
|
+
evidenceTimestampMs,
|
|
190
|
+
completionGraceMs: this.actionCompletionGraceMs,
|
|
191
|
+
}),
|
|
192
|
+
timestamps: { occurredAtMs: observedAtMs, observedAtMs },
|
|
193
|
+
payload: {
|
|
194
|
+
kind: 'evidence',
|
|
195
|
+
stream: 'logcat',
|
|
196
|
+
record: {
|
|
197
|
+
lines: Array.isArray(batch.lines) ? batch.lines : [],
|
|
198
|
+
count: Number(batch.count || batch.lines?.length || 0),
|
|
199
|
+
dropped: Number(batch.dropped || 0),
|
|
200
|
+
buffers: Array.isArray(batch.buffers) ? batch.buffers : [],
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
return { ...appended, partition: 'device-log', targetKey: target.key };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
readHistory(command, args = {}) {
|
|
208
|
+
const target = targetFor(command, args);
|
|
209
|
+
const history = historyDescriptor(command, args);
|
|
210
|
+
if (!history) {
|
|
211
|
+
return { ok: false, error: 'fact_history_unsupported', command };
|
|
212
|
+
}
|
|
213
|
+
const queried = this.cache.query({
|
|
214
|
+
partitions: history.partitions,
|
|
215
|
+
target: target.key,
|
|
216
|
+
cursor: args.factCursor ?? args.cursor ?? null,
|
|
217
|
+
limit: args.limit === undefined ? 100 : Number(args.limit),
|
|
218
|
+
});
|
|
219
|
+
if (!queried.ok) {
|
|
220
|
+
return {
|
|
221
|
+
...queried,
|
|
222
|
+
type: history.type,
|
|
223
|
+
_factCache: {
|
|
224
|
+
history: true,
|
|
225
|
+
gap: Boolean(queried.gap),
|
|
226
|
+
cursorExpired: Boolean(queried.cursorExpired),
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const matching = queried.items.filter((fact) => (
|
|
231
|
+
history.streams.has(fact.payload?.stream)
|
|
232
|
+
|| (history.includeActions && fact.partition === 'action' && fact.payload?.kind === 'execution')
|
|
233
|
+
));
|
|
234
|
+
const items = matching.map((fact) => ({
|
|
235
|
+
...cloneRecord(fact.partition === 'action' ? fact.payload : fact.payload.record),
|
|
236
|
+
_fact: {
|
|
237
|
+
globalSeq: fact.globalSeq,
|
|
238
|
+
partition: fact.partition,
|
|
239
|
+
runtimeEpoch: fact.runtimeEpoch,
|
|
240
|
+
actionId: fact.actionId,
|
|
241
|
+
timestamps: fact.timestamps,
|
|
242
|
+
},
|
|
243
|
+
}));
|
|
244
|
+
return {
|
|
245
|
+
ok: true,
|
|
246
|
+
type: history.type,
|
|
247
|
+
items,
|
|
248
|
+
count: items.length,
|
|
249
|
+
limit: args.limit === undefined ? 100 : Number(args.limit),
|
|
250
|
+
updatedAtMs: this.now(),
|
|
251
|
+
_factCache: {
|
|
252
|
+
history: true,
|
|
253
|
+
cursor: queried.cursor,
|
|
254
|
+
gap: false,
|
|
255
|
+
cursorExpired: false,
|
|
256
|
+
hasMore: queried.hasMore,
|
|
257
|
+
scannedCount: queried.count,
|
|
258
|
+
targetKey: target.key,
|
|
259
|
+
partitions: history.partitions,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
status() {
|
|
265
|
+
return this.cache.status();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
observeRuntimeEpoch(targetKey, result, explicitEpoch) {
|
|
269
|
+
const discovered = explicitEpoch
|
|
270
|
+
|| result?.debugBridge?.runtimeEpoch
|
|
271
|
+
|| result?.runtimeEpoch
|
|
272
|
+
|| (result?.session?.connectedAtMs
|
|
273
|
+
? `web-${result.session.connectedAtMs}`
|
|
274
|
+
: null);
|
|
275
|
+
if (discovered) this.runtimeEpochs.set(targetKey, String(discovered));
|
|
276
|
+
if (!this.runtimeEpochs.has(targetKey)) {
|
|
277
|
+
this.runtimeEpochs.set(targetKey, `${this.hostEpoch}:${targetKey}`);
|
|
278
|
+
}
|
|
279
|
+
return this.runtimeEpochs.get(targetKey);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
nextActionId() {
|
|
283
|
+
this.actionSequence += 1;
|
|
284
|
+
return `action-${this.now()}-${this.actionSequence}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
rememberEvidence(key) {
|
|
288
|
+
this.seenEvidence.add(key);
|
|
289
|
+
this.seenEvidenceOrder.push(key);
|
|
290
|
+
while (this.seenEvidenceOrder.length > this.maxSeenEvidence) {
|
|
291
|
+
this.seenEvidence.delete(this.seenEvidenceOrder.shift());
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function evidenceDescriptors(command, result) {
|
|
297
|
+
const normalized = String(command || '').toLowerCase();
|
|
298
|
+
const stream = captureStream(normalized);
|
|
299
|
+
if (stream) {
|
|
300
|
+
const items = Array.isArray(result.items) ? result.items : [];
|
|
301
|
+
return items.map((record) => ({
|
|
302
|
+
partition: partitionForStream(stream, record),
|
|
303
|
+
stream,
|
|
304
|
+
record,
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
if (UI_EVIDENCE_COMMANDS.has(normalized)) {
|
|
308
|
+
return [{ partition: 'ui', stream: normalized, record: result }];
|
|
309
|
+
}
|
|
310
|
+
if (normalized.endsWith('status')) {
|
|
311
|
+
return [{ partition: 'state-event', stream: normalized, record: result }];
|
|
312
|
+
}
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function captureStream(command) {
|
|
317
|
+
if (command === 'logs' || command === 'ios-logs' || command === 'web-logs') return 'logs';
|
|
318
|
+
if (command === 'network' || command === 'ios-network' || command === 'web-network') return 'network';
|
|
319
|
+
if (command === 'state' || command === 'ios-state' || command === 'web-state') return 'state';
|
|
320
|
+
if (command === 'events' || command === 'ios-events' || command === 'web-events') return 'events';
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isMobileCaptureCommand(command) {
|
|
325
|
+
const normalized = String(command || '').toLowerCase();
|
|
326
|
+
return normalized === 'logs'
|
|
327
|
+
|| normalized === 'ios-logs'
|
|
328
|
+
|| normalized === 'network'
|
|
329
|
+
|| normalized === 'ios-network'
|
|
330
|
+
|| normalized === 'state'
|
|
331
|
+
|| normalized === 'ios-state'
|
|
332
|
+
|| normalized === 'events'
|
|
333
|
+
|| normalized === 'ios-events';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function partitionForStream(stream, record) {
|
|
337
|
+
if (stream === 'logs') return 'app-log';
|
|
338
|
+
if (stream === 'network') return 'network';
|
|
339
|
+
if (stream === 'events' && isUiRecord(record)) return 'ui';
|
|
340
|
+
return 'state-event';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function isUiRecord(record) {
|
|
344
|
+
const category = String(record?.category || '').toLowerCase();
|
|
345
|
+
const name = String(record?.name || '').toLowerCase();
|
|
346
|
+
return category === 'ui'
|
|
347
|
+
|| category.startsWith('ui.')
|
|
348
|
+
|| name === 'ui.changed'
|
|
349
|
+
|| name === 'ui.stable'
|
|
350
|
+
|| name.startsWith('ui.');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function shouldProjectPrivateUiEvidence(command, descriptor) {
|
|
354
|
+
const normalized = String(command || '').toLowerCase();
|
|
355
|
+
return descriptor.partition === 'ui'
|
|
356
|
+
|| UI_EVIDENCE_COMMANDS.has(normalized)
|
|
357
|
+
|| normalized.endsWith('status');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function privacyProjectUiEvidence(record) {
|
|
361
|
+
return projectPrivateUiValue(record, {
|
|
362
|
+
inheritedSecure: false,
|
|
363
|
+
ancestors: new WeakSet(),
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function projectPrivateUiValue(value, state) {
|
|
368
|
+
if (value === null || value === undefined) return value ?? null;
|
|
369
|
+
if (typeof value !== 'object') return value;
|
|
370
|
+
if (value instanceof Date) return value.toISOString();
|
|
371
|
+
if (Buffer.isBuffer(value)) return { type: 'buffer', byteLength: value.length };
|
|
372
|
+
if (state.ancestors.has(value)) return '[CIRCULAR]';
|
|
373
|
+
|
|
374
|
+
state.ancestors.add(value);
|
|
375
|
+
if (Array.isArray(value)) {
|
|
376
|
+
const projected = value.map((entry) => projectPrivateUiValue(entry, {
|
|
377
|
+
inheritedSecure: false,
|
|
378
|
+
ancestors: state.ancestors,
|
|
379
|
+
}));
|
|
380
|
+
state.ancestors.delete(value);
|
|
381
|
+
return projected;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const secure = state.inheritedSecure
|
|
385
|
+
|| hasAndroidPasswordInputType(value.inputType)
|
|
386
|
+
|| hasPasswordType(value)
|
|
387
|
+
|| hasSensitiveInputIdentity(value)
|
|
388
|
+
|| isTruePrivacyFlag(value.input?.secure)
|
|
389
|
+
|| hasDirectSecureFlag(value)
|
|
390
|
+
|| /\bisobscured\b|\bobscured\b/i.test(String(value.flags || ''));
|
|
391
|
+
const projected = {};
|
|
392
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
393
|
+
if (secure && isSecureTextKey(key)) {
|
|
394
|
+
projected[key] = REDACTED;
|
|
395
|
+
projected[`${key}Length`] = secureTextLength(value, key, childValue);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
projected[key] = projectPrivateUiValue(childValue, {
|
|
399
|
+
inheritedSecure: false,
|
|
400
|
+
ancestors: state.ancestors,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (secure) projected.secure = true;
|
|
404
|
+
state.ancestors.delete(value);
|
|
405
|
+
return projected;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function hasAndroidPasswordInputType(value) {
|
|
409
|
+
const inputType = Number(value);
|
|
410
|
+
if (!Number.isSafeInteger(inputType)) return false;
|
|
411
|
+
const inputClass = inputType & 0x0f;
|
|
412
|
+
const variation = inputType & 0x0ff0;
|
|
413
|
+
return (inputClass === 0x01 && [0x80, 0x90, 0xe0].includes(variation))
|
|
414
|
+
|| (inputClass === 0x02 && variation === 0x10);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function hasPasswordType(value) {
|
|
418
|
+
return normalizePrivacyKey(value?.type) === 'password'
|
|
419
|
+
|| /password|passcode|secure/i.test(String(value?.inputType || ''))
|
|
420
|
+
|| [value?.className, value?.simpleClassName, value?.type, value?.role]
|
|
421
|
+
.some((candidate) => /securetext|passwordfield|passwordinput/i.test(String(candidate || '')));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function hasSensitiveInputIdentity(value) {
|
|
425
|
+
const inputShape = [
|
|
426
|
+
value?.className,
|
|
427
|
+
value?.simpleClassName,
|
|
428
|
+
value?.tag,
|
|
429
|
+
value?.role,
|
|
430
|
+
value?.type,
|
|
431
|
+
].some((candidate) => (
|
|
432
|
+
/edittext|textfield|textinput|securefield|searchfield|^input$|^textarea$|textbox/i
|
|
433
|
+
.test(String(candidate || ''))
|
|
434
|
+
));
|
|
435
|
+
if (!inputShape) return false;
|
|
436
|
+
return [
|
|
437
|
+
value?.resourceName,
|
|
438
|
+
value?.resourceId,
|
|
439
|
+
value?.id,
|
|
440
|
+
value?.name,
|
|
441
|
+
value?.identifier,
|
|
442
|
+
value?.autocomplete,
|
|
443
|
+
value?.textContentType,
|
|
444
|
+
value?.placeholder,
|
|
445
|
+
].some((candidate) => (
|
|
446
|
+
/password|passwd|pwd|passcode|current-password|new-password/i
|
|
447
|
+
.test(String(candidate || ''))
|
|
448
|
+
));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function isTruePrivacyFlag(value) {
|
|
452
|
+
return value === true || value === 1 || String(value).toLowerCase() === 'true';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function hasDirectSecureFlag(value) {
|
|
456
|
+
return Object.entries(value).some(([key, flag]) => (
|
|
457
|
+
SECURE_FLAG_KEYS.has(normalizePrivacyKey(key)) && isTruePrivacyFlag(flag)
|
|
458
|
+
));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function isSecureTextKey(key) {
|
|
462
|
+
return SECURE_TEXT_KEYS.has(normalizePrivacyKey(key));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function secureTextLength(container, key, value) {
|
|
466
|
+
const explicit = Number(container?.[`${key}Length`]);
|
|
467
|
+
if (Number.isSafeInteger(explicit) && explicit >= 0) return explicit;
|
|
468
|
+
if (typeof value === 'string') {
|
|
469
|
+
const encodedLength = /^\[secure:length=(\d+)]$/i.exec(value.trim());
|
|
470
|
+
if (encodedLength) return Number(encodedLength[1]);
|
|
471
|
+
return value === REDACTED ? 0 : value.length;
|
|
472
|
+
}
|
|
473
|
+
if (Array.isArray(value) || Buffer.isBuffer(value)) return value.length;
|
|
474
|
+
return value === null || value === undefined ? 0 : String(value).length;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function normalizePrivacyKey(value) {
|
|
478
|
+
return String(value || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function historyDescriptor(command, args = {}) {
|
|
482
|
+
const normalized = String(command || '').toLowerCase();
|
|
483
|
+
const stream = captureStream(normalized);
|
|
484
|
+
if (stream === 'logs') return descriptor('logs', ['app-log'], ['logs']);
|
|
485
|
+
if (stream === 'network') return descriptor('network', ['network'], ['network']);
|
|
486
|
+
if (stream === 'state') return descriptor('state', ['state-event'], ['state']);
|
|
487
|
+
if (stream === 'events') {
|
|
488
|
+
const includeActions = args.includeActions === true || args.includeActions === 'true';
|
|
489
|
+
return descriptor(
|
|
490
|
+
'events',
|
|
491
|
+
includeActions ? ['ui', 'state-event', 'action'] : ['ui', 'state-event'],
|
|
492
|
+
['events'],
|
|
493
|
+
{ includeActions },
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (normalized === 'logcat') return descriptor('logcat', ['device-log'], ['logcat']);
|
|
497
|
+
if (UI_EVIDENCE_COMMANDS.has(normalized)) return descriptor(normalized, ['ui'], [normalized]);
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function descriptor(type, partitions, streams, extra = {}) {
|
|
502
|
+
return { type, partitions, streams: new Set(streams), ...extra };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function summarizeArgs(args, depth = 0, ancestors = new WeakSet()) {
|
|
506
|
+
if (!args || typeof args !== 'object') return {};
|
|
507
|
+
if (ancestors.has(args)) return '[circular]';
|
|
508
|
+
ancestors.add(args);
|
|
509
|
+
const result = {};
|
|
510
|
+
for (const [key, value] of Object.entries(args || {})) {
|
|
511
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
512
|
+
if (['text', 'value', 'script', 'payload', 'requestbody', 'responsebody'].includes(normalized)) {
|
|
513
|
+
result[`${key}Length`] = value === undefined || value === null ? 0 : String(value).length;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
result[key] = boundedValue(value, depth + 1, ancestors);
|
|
517
|
+
}
|
|
518
|
+
ancestors.delete(args);
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function boundedValue(value, depth, ancestors = new WeakSet()) {
|
|
523
|
+
if (value === null || value === undefined) return value ?? null;
|
|
524
|
+
if (typeof value === 'string') return value.slice(0, 300);
|
|
525
|
+
if (typeof value !== 'object') return value;
|
|
526
|
+
if (depth >= 3) return '[truncated]';
|
|
527
|
+
if (ancestors.has(value)) return '[circular]';
|
|
528
|
+
if (Array.isArray(value)) {
|
|
529
|
+
ancestors.add(value);
|
|
530
|
+
const result = value.slice(0, 20).map((item) => boundedValue(item, depth + 1, ancestors));
|
|
531
|
+
ancestors.delete(value);
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
return summarizeArgs(value, depth, ancestors);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function summarizeResult(result) {
|
|
538
|
+
if (result === undefined || result === null) return null;
|
|
539
|
+
if (typeof result !== 'object' || Buffer.isBuffer(result)) {
|
|
540
|
+
return { type: Buffer.isBuffer(result) ? 'buffer' : typeof result };
|
|
541
|
+
}
|
|
542
|
+
const summary = {};
|
|
543
|
+
for (const key of [
|
|
544
|
+
'ok', 'error', 'message', 'action', 'source', 'transport', 'activity', 'component',
|
|
545
|
+
'path', 'count', 'nodeCount', 'sessionId', 'targetId', 'packageName', 'bundleId',
|
|
546
|
+
'textLength', 'matched', 'target', 'windowType', 'x', 'y', 'handledDown', 'handledUp',
|
|
547
|
+
'verified', 'inconclusive', 'dispatched', 'ambiguous', 'settled', 'executionReceipt', 'executionReceipts',
|
|
548
|
+
]) {
|
|
549
|
+
if (result[key] !== undefined) summary[key] = boundedValue(result[key], 0);
|
|
550
|
+
}
|
|
551
|
+
return summary;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function appIdentity(target, result) {
|
|
555
|
+
return {
|
|
556
|
+
...(result?.app && typeof result.app === 'object' ? result.app : {}),
|
|
557
|
+
kind: target.kind,
|
|
558
|
+
...(target.serial !== undefined ? { serial: target.serial } : {}),
|
|
559
|
+
...(target.packageName !== undefined ? { packageName: target.packageName } : {}),
|
|
560
|
+
...(target.deviceId !== undefined ? { deviceId: target.deviceId } : {}),
|
|
561
|
+
...(target.bundleId !== undefined ? { bundleId: target.bundleId } : {}),
|
|
562
|
+
...(target.sessionId !== undefined ? { sessionId: target.sessionId } : {}),
|
|
563
|
+
...(target.targetId !== undefined ? { targetId: target.targetId } : {}),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function executionStatus(result, error) {
|
|
568
|
+
if (error || result?.ok === false) return 'failed';
|
|
569
|
+
if (result?.verified === true) return 'verified';
|
|
570
|
+
if (result === undefined || result === null || result?.inconclusive === true) return 'inconclusive';
|
|
571
|
+
return 'completed';
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function finiteTimestamp(value, fallback) {
|
|
575
|
+
return optionalTimestamp(value) ?? fallback;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function optionalTimestamp(value) {
|
|
579
|
+
if (value === undefined || value === null || value === '') return null;
|
|
580
|
+
const number = Number(value);
|
|
581
|
+
return Number.isSafeInteger(number) ? number : null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function actionIdForEvidence({
|
|
585
|
+
context = {},
|
|
586
|
+
fallbackActionId,
|
|
587
|
+
runtimeActionId,
|
|
588
|
+
evidenceTimestampMs,
|
|
589
|
+
completionGraceMs,
|
|
590
|
+
}) {
|
|
591
|
+
const explicitRuntimeActionId = nullableActionId(runtimeActionId);
|
|
592
|
+
if (explicitRuntimeActionId !== null) return explicitRuntimeActionId;
|
|
593
|
+
const timeline = normalizedActionTimeline(context.actionTimeline);
|
|
594
|
+
const explicitActionId = context.actionId ?? fallbackActionId ?? null;
|
|
595
|
+
if (evidenceTimestampMs === null) {
|
|
596
|
+
return latestTimelineActionId(timeline) ?? nullableActionId(explicitActionId);
|
|
597
|
+
}
|
|
598
|
+
if (timeline.length === 0) return nullableActionId(explicitActionId);
|
|
599
|
+
|
|
600
|
+
const exact = timeline.filter((action) => timestampInActionInterval(action, evidenceTimestampMs, timeline));
|
|
601
|
+
if (exact.length === 1) return exact[0].actionId;
|
|
602
|
+
if (exact.length > 1) return null;
|
|
603
|
+
|
|
604
|
+
const grace = timeline.filter((action) => timestampInCompletionGrace(
|
|
605
|
+
action,
|
|
606
|
+
evidenceTimestampMs,
|
|
607
|
+
timeline,
|
|
608
|
+
completionGraceMs,
|
|
609
|
+
));
|
|
610
|
+
return grace.length === 1 ? grace[0].actionId : null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function normalizedActionTimeline(value) {
|
|
614
|
+
if (!Array.isArray(value)) return [];
|
|
615
|
+
return value.flatMap((candidate, index) => {
|
|
616
|
+
const actionId = nullableActionId(candidate?.actionId);
|
|
617
|
+
const requestedAtMs = optionalTimestamp(candidate?.requestedAtMs);
|
|
618
|
+
const startedAtMs = optionalTimestamp(candidate?.startedAtMs);
|
|
619
|
+
const completedAtMs = optionalTimestamp(candidate?.completedAtMs);
|
|
620
|
+
const startAtMs = startedAtMs ?? (completedAtMs === null ? null : requestedAtMs);
|
|
621
|
+
if (
|
|
622
|
+
actionId === null
|
|
623
|
+
|| (requestedAtMs === null && startedAtMs === null && completedAtMs === null)
|
|
624
|
+
) return [];
|
|
625
|
+
return [{
|
|
626
|
+
actionId,
|
|
627
|
+
requestedAtMs,
|
|
628
|
+
startedAtMs,
|
|
629
|
+
completedAtMs,
|
|
630
|
+
startAtMs,
|
|
631
|
+
order: index,
|
|
632
|
+
}];
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function timestampInActionInterval(action, timestampMs, timeline) {
|
|
637
|
+
if (action.startAtMs === null || timestampMs < action.startAtMs) return false;
|
|
638
|
+
if (action.completedAtMs !== null) return timestampMs <= action.completedAtMs;
|
|
639
|
+
const nextStartAtMs = nextActionStartAtMs(action, timeline);
|
|
640
|
+
return nextStartAtMs === null || timestampMs < nextStartAtMs;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function timestampInCompletionGrace(action, timestampMs, timeline, completionGraceMs) {
|
|
644
|
+
if (action.completedAtMs === null || timestampMs <= action.completedAtMs) return false;
|
|
645
|
+
if (timestampMs > action.completedAtMs + completionGraceMs) return false;
|
|
646
|
+
const nextStartAtMs = nextActionStartAtMs(action, timeline);
|
|
647
|
+
return nextStartAtMs === null || timestampMs < nextStartAtMs;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function nextActionStartAtMs(action, timeline) {
|
|
651
|
+
let next = null;
|
|
652
|
+
for (const candidate of timeline) {
|
|
653
|
+
if (
|
|
654
|
+
candidate === action
|
|
655
|
+
|| candidate.startAtMs === null
|
|
656
|
+
|| candidate.startAtMs <= action.startAtMs
|
|
657
|
+
) continue;
|
|
658
|
+
if (next === null || candidate.startAtMs < next) next = candidate.startAtMs;
|
|
659
|
+
}
|
|
660
|
+
return next;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function latestTimelineActionId(timeline) {
|
|
664
|
+
let latest = null;
|
|
665
|
+
for (const action of timeline) {
|
|
666
|
+
const atMs = action.completedAtMs ?? action.startedAtMs ?? action.requestedAtMs;
|
|
667
|
+
if (atMs === null) continue;
|
|
668
|
+
if (!latest || atMs > latest.atMs || (atMs === latest.atMs && action.order > latest.order)) {
|
|
669
|
+
latest = { actionId: action.actionId, atMs, order: action.order };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return latest?.actionId ?? null;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function nullableActionId(value) {
|
|
676
|
+
return value === undefined || value === null ? null : String(value);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function cloneRecord(record) {
|
|
680
|
+
if (!record || typeof record !== 'object') return { value: record };
|
|
681
|
+
return JSON.parse(JSON.stringify(record));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
module.exports = {
|
|
685
|
+
FactRecorder,
|
|
686
|
+
captureStream,
|
|
687
|
+
historyDescriptor,
|
|
688
|
+
isMobileCaptureCommand,
|
|
689
|
+
isUiRecord,
|
|
690
|
+
summarizeArgs,
|
|
691
|
+
};
|