@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.
Files changed (156) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +306 -53
  4. package/bin/ai-app-bridge.js +51 -4504
  5. package/bin/android-permissions.js +152 -0
  6. package/bin/android-uia-xml.js +74 -0
  7. package/bin/artifact-paths.js +127 -1
  8. package/bin/bridge-forward.js +56 -0
  9. package/bin/command-discovery.js +86 -0
  10. package/bin/command-errors.js +60 -0
  11. package/bin/command-registry.js +504 -0
  12. package/bin/command-request.js +14 -0
  13. package/bin/command-router.js +80 -0
  14. package/bin/connection-cache.js +119 -0
  15. package/bin/device-provider.js +2934 -0
  16. package/bin/execution-host.js +596 -0
  17. package/bin/execution-runtime.js +101 -0
  18. package/bin/fact-codec.js +321 -0
  19. package/bin/fact-recorder.js +691 -0
  20. package/bin/fact-store.js +226 -0
  21. package/bin/feedback-probe.js +285 -0
  22. package/bin/intent/install-intent.js +267 -0
  23. package/bin/intent/intent-action-executor.js +129 -0
  24. package/bin/intent/intent-autonomous-adapter.js +46 -0
  25. package/bin/intent/intent-capture-port.js +64 -0
  26. package/bin/intent/intent-entry.js +226 -0
  27. package/bin/intent/intent-errors.js +28 -0
  28. package/bin/intent/intent-evidence-store.js +121 -0
  29. package/bin/intent/intent-lifetime.js +62 -0
  30. package/bin/intent/intent-observation-target.js +33 -0
  31. package/bin/intent/intent-observer.js +194 -0
  32. package/bin/intent/intent-production-adapter.js +334 -0
  33. package/bin/intent/intent-provider.js +27 -0
  34. package/bin/intent/intent-runtime.js +63 -0
  35. package/bin/intent/intent-worker.js +432 -0
  36. package/bin/intent/ios-intent-adapter.js +90 -0
  37. package/bin/intent/permission-intent.js +239 -0
  38. package/bin/intent/web-intent-adapter.js +31 -0
  39. package/bin/ios-device-outcome.js +47 -0
  40. package/bin/ios-execution.js +108 -0
  41. package/bin/ios-provider.js +497 -583
  42. package/bin/ios-runtime-binding.js +54 -0
  43. package/bin/ios-wda-execution.js +70 -0
  44. package/bin/ios-wda-port.js +98 -0
  45. package/bin/ios-wda-project.js +79 -0
  46. package/bin/mcp-server.js +83 -1040
  47. package/bin/mmap-scan-index.js +329 -0
  48. package/bin/observation-collector.js +862 -0
  49. package/bin/runtime-client.js +154 -0
  50. package/bin/runtime-directory.js +107 -0
  51. package/bin/runtime-protocol.js +36 -0
  52. package/bin/script/bounded-script-registry.js +103 -0
  53. package/bin/script/node-runtime-adapter.js +233 -0
  54. package/bin/script/progress-projector.js +63 -0
  55. package/bin/script/python-runtime-adapter.js +111 -0
  56. package/bin/script/rolling-summary.js +134 -0
  57. package/bin/script/script-agent-port.js +40 -0
  58. package/bin/script/script-assert.js +95 -0
  59. package/bin/script/script-capture-port.js +76 -0
  60. package/bin/script/script-catalog.js +85 -0
  61. package/bin/script/script-durable-restore.js +195 -0
  62. package/bin/script/script-entry-code.js +26 -0
  63. package/bin/script/script-entry-route.js +38 -0
  64. package/bin/script/script-entry.js +3 -0
  65. package/bin/script/script-errors.js +29 -0
  66. package/bin/script/script-evidence-store.js +22 -0
  67. package/bin/script/script-format-removed.js +26 -0
  68. package/bin/script/script-host-port.js +397 -0
  69. package/bin/script/script-ledger.js +64 -0
  70. package/bin/script/script-result.js +57 -0
  71. package/bin/script/script-sdk.js +152 -0
  72. package/bin/script/script-sdk.py +153 -0
  73. package/bin/script/script-session-channel.js +127 -0
  74. package/bin/script/script-spec.js +86 -0
  75. package/bin/script/script-supervisor.js +919 -0
  76. package/bin/script/templates/checkpoint-reentry.js +13 -0
  77. package/bin/segment-index.js +481 -0
  78. package/bin/segmented-fact-store.js +1571 -0
  79. package/bin/shared-kernel/android-h5-target.js +10 -0
  80. package/bin/shared-kernel/android-install-execution.js +176 -0
  81. package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
  82. package/bin/shared-kernel/android-shell-execution.js +195 -0
  83. package/bin/shared-kernel/argument-schema.js +117 -0
  84. package/bin/shared-kernel/canonical-path.js +17 -0
  85. package/bin/shared-kernel/device-acknowledgements.js +53 -0
  86. package/bin/shared-kernel/device-completion-history.js +52 -0
  87. package/bin/shared-kernel/device-mutation-lease.js +219 -0
  88. package/bin/shared-kernel/device-ownership-recovery.js +95 -0
  89. package/bin/shared-kernel/device-ownership-store.js +94 -0
  90. package/bin/shared-kernel/evidence-adapters.js +251 -0
  91. package/bin/shared-kernel/evidence-archive.js +329 -0
  92. package/bin/shared-kernel/evidence-recording.js +131 -0
  93. package/bin/shared-kernel/evidence-schema.js +194 -0
  94. package/bin/shared-kernel/evidence-store.js +190 -0
  95. package/bin/shared-kernel/execution-admission.js +22 -0
  96. package/bin/shared-kernel/execution-contracts.js +171 -0
  97. package/bin/shared-kernel/execution-io.js +106 -0
  98. package/bin/shared-kernel/execution-ledger.js +125 -0
  99. package/bin/shared-kernel/execution-scope.js +87 -0
  100. package/bin/shared-kernel/execution-target.js +115 -0
  101. package/bin/shared-kernel/flutter-execution.js +13 -0
  102. package/bin/shared-kernel/flutter-h5-port.js +60 -0
  103. package/bin/shared-kernel/flutter-h5-target.js +9 -0
  104. package/bin/shared-kernel/flutter-target.js +75 -0
  105. package/bin/shared-kernel/h5-execution.js +11 -0
  106. package/bin/shared-kernel/h5-target.js +31 -0
  107. package/bin/shared-kernel/host-fact-store.js +49 -0
  108. package/bin/shared-kernel/ios-h5-target.js +9 -0
  109. package/bin/shared-kernel/ios-native-target.js +71 -0
  110. package/bin/shared-kernel/live-capture-query.js +115 -0
  111. package/bin/shared-kernel/managed-sdk-execution.js +78 -0
  112. package/bin/shared-kernel/native-execution.js +13 -0
  113. package/bin/shared-kernel/native-target.js +156 -0
  114. package/bin/shared-kernel/provider-command-contracts.js +55 -0
  115. package/bin/shared-kernel/recorded-payload-archive.js +195 -0
  116. package/bin/shared-kernel/request-context.js +35 -0
  117. package/bin/shared-kernel/semantic-node.js +55 -0
  118. package/bin/shared-kernel/summary-transformer.js +352 -0
  119. package/bin/shared-kernel/target-lease-protocol.js +47 -0
  120. package/bin/shared-kernel/text-wait.js +111 -0
  121. package/bin/shared-kernel/uia-execution.js +96 -0
  122. package/bin/shared-kernel/uia-protocol.js +214 -0
  123. package/bin/shared-kernel/uia-runtime-port.js +377 -0
  124. package/bin/shared-kernel/uia-target.js +39 -0
  125. package/bin/shared-kernel/web-dom-target.js +44 -0
  126. package/bin/shared-kernel/xml-attributes.js +25 -0
  127. package/bin/target-execution.js +275 -0
  128. package/bin/web/command-schema.js +60 -0
  129. package/bin/web/session-store.js +157 -0
  130. package/bin/web-provider.js +334 -553
  131. package/docs/COMMAND_CONTRACT.md +1563 -0
  132. package/docs/EVIDENCE_ARCHIVE.md +214 -0
  133. package/docs/INTENT_FOREGROUND.md +71 -0
  134. package/docs/INTENT_NATIVE_EDITING.md +79 -0
  135. package/docs/RELEASE.md +59 -0
  136. package/docs/SCRIPT_AUTHORING.md +489 -0
  137. package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
  138. package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
  139. package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
  140. package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
  141. package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
  142. package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
  143. package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
  144. package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
  145. package/package.json +60 -5
  146. package/runtime/ios-wda/AABWDABinding.h +19 -0
  147. package/runtime/ios-wda/AABWDABinding.m +97 -0
  148. package/runtime/ios-wda/AABWDAExecution.h +26 -0
  149. package/runtime/ios-wda/AABWDAExecution.m +172 -0
  150. package/runtime/ios-wda/AABWDAIntegration.h +71 -0
  151. package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
  152. package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
  153. package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
  154. package/runtime/uia/ai-app-bridge-uia.jar +0 -0
  155. package/runtime/uia/manifest.json +22 -0
  156. package/skills/ai-app-bridge-use/SKILL.md +23 -360
@@ -0,0 +1,251 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ function createMemoryEvidenceAdapter({ maxBytes = Number.MAX_SAFE_INTEGER, fault = null } = {}) {
7
+ const records = new Map();
8
+ let sequence = 0;
9
+ const adapter = {
10
+ record(envelope) {
11
+ if (fault === 'throw') {
12
+ throw new Error('writer_crash');
13
+ }
14
+ if (fault === 'enospc') {
15
+ const error = new Error('ENOSPC');
16
+ error.code = 'ENOSPC';
17
+ return { ok: false, error: 'ENOSPC' };
18
+ }
19
+ if (fault === 'hang') {
20
+ return new Promise(() => {});
21
+ }
22
+ const bytes = Buffer.byteLength(JSON.stringify(envelope));
23
+ if (bytes > maxBytes) {
24
+ return { ok: false, error: 'payload_too_large', bytes, maxBytes };
25
+ }
26
+ sequence += 1;
27
+ const stored = fault === 'corrupt_manifest'
28
+ ? { ...envelope, checksum: '0'.repeat(64) }
29
+ : { ...envelope };
30
+ records.set(stored.evidenceId, stored);
31
+ return { ok: true, stored: true, globalSeq: sequence, evidenceId: stored.evidenceId };
32
+ },
33
+ offer(envelope) {
34
+ return { accepted: true, completion: Promise.resolve().then(() => adapter.record(envelope)) };
35
+ },
36
+ readById(evidenceId) {
37
+ return records.get(evidenceId) || null;
38
+ },
39
+ list({ namespace, operationId } = {}) {
40
+ return [...records.values()].filter((item) => (
41
+ (!namespace || item.namespace === namespace)
42
+ && (!operationId || item.operationId === operationId)
43
+ ));
44
+ },
45
+ status() {
46
+ if (fault === 'corrupt_manifest') {
47
+ return { ok: false, error: 'corrupt_manifest', count: records.size };
48
+ }
49
+ return { ok: true, adapter: 'memory', count: records.size, degraded: false };
50
+ },
51
+ close() {
52
+ records.clear();
53
+ },
54
+ };
55
+ return adapter;
56
+ }
57
+
58
+ function createSegmentedEvidenceAdapter(factStore, { ownsStore = false } = {}) {
59
+ if (!factStore || typeof factStore.record !== 'function' || typeof factStore.read !== 'function') {
60
+ throw new TypeError('segmented evidence adapter requires a FactStore');
61
+ }
62
+ function factFor(envelope) {
63
+ return {
64
+ partition: partitionFor(envelope.kind),
65
+ targetKey: `evidence:${envelope.namespace}:${envelope.operationId}`,
66
+ app: { platform: 'host', packageName: envelope.namespace },
67
+ runtimeEpoch: String(envelope.operationId),
68
+ actionId: envelope.evidenceId,
69
+ timestamps: {
70
+ occurredAtMs: envelope.committedAtMs,
71
+ observedAtMs: envelope.committedAtMs,
72
+ },
73
+ payload: envelope,
74
+ };
75
+ }
76
+ const adapter = {
77
+ record(envelope) {
78
+ try {
79
+ const result = factStore.record(factFor(envelope), { durability: 'sync' });
80
+ const receipt = result.receipt || result.receipts?.[0] || result;
81
+ if (receipt?.ok === false || result.ok === false) {
82
+ return { ok: false, error: receipt?.error || result.error || 'persist_failed' };
83
+ }
84
+ return { ok: true, stored: true, evidenceId: envelope.evidenceId, globalSeq: receipt.globalSeq };
85
+ } catch (error) {
86
+ return {
87
+ ok: false,
88
+ error: error.code || 'persist_failed',
89
+ detail: error.message || String(error),
90
+ };
91
+ }
92
+ },
93
+ offer(envelope) {
94
+ if (typeof factStore.offer !== 'function') {
95
+ return { accepted: false, reason: 'async_offer_unsupported' };
96
+ }
97
+ const offered = factStore.offer(factFor(envelope), { durability: 'sync' });
98
+ if (!offered.accepted) return offered;
99
+ return {
100
+ ...offered,
101
+ completion: offered.completion.then((result) => {
102
+ const receipt = result.receipt || result.receipts?.[0] || result;
103
+ if (receipt?.ok === false || result.ok === false) {
104
+ return { ok: false, stored: false, error: receipt?.error || result.error || 'persist_failed' };
105
+ }
106
+ return {
107
+ ok: true,
108
+ stored: true,
109
+ evidenceId: envelope.evidenceId,
110
+ globalSeq: receipt.globalSeq,
111
+ };
112
+ }),
113
+ };
114
+ },
115
+ readById(evidenceId) {
116
+ const page = factStore.read({ actionId: evidenceId, limit: 1 });
117
+ if (!page.ok) throw Object.assign(new Error(page.error || 'Evidence store read failed.'), { code: 'evidence_read_failed' });
118
+ const item = page.items?.[0];
119
+ return item?.payload || null;
120
+ },
121
+ list({ namespace, operationId } = {}) {
122
+ const items = [];
123
+ let cursor;
124
+ do {
125
+ const page = factStore.read({
126
+ targetKey: operationId ? `evidence:${namespace}:${operationId}` : undefined,
127
+ cursor,
128
+ limit: 1_000,
129
+ });
130
+ if (!page.ok) throw Object.assign(new Error(page.error || 'Evidence store read failed.'), { code: 'evidence_read_failed' });
131
+ items.push(...(page.items || []));
132
+ cursor = page.hasMore ? page.cursor : null;
133
+ } while (cursor);
134
+ return items
135
+ .map((item) => item.payload)
136
+ .filter((payload) => payload
137
+ && (!namespace || payload.namespace === namespace)
138
+ && (!operationId || payload.operationId === operationId));
139
+ },
140
+ status() {
141
+ return factStore.status();
142
+ },
143
+ close() {
144
+ if (ownsStore) factStore.close?.();
145
+ },
146
+ };
147
+ return adapter;
148
+ }
149
+
150
+ function createLegacyFactStoreAdapter(factStore) {
151
+ if (
152
+ !factStore
153
+ || typeof factStore.record !== 'function'
154
+ || typeof factStore.read !== 'function'
155
+ || typeof factStore.status !== 'function'
156
+ ) {
157
+ throw new TypeError('legacy fact adapter requires a FactStore');
158
+ }
159
+ return {
160
+ append(partitionOrFact, maybeFact) {
161
+ const fact = maybeFact === undefined
162
+ ? partitionOrFact
163
+ : { ...maybeFact, partition: String(partitionOrFact) };
164
+ const result = factStore.record(fact, { durability: 'sync' });
165
+ return result.receipt || result.receipts?.[0] || result;
166
+ },
167
+ query(query = {}) {
168
+ return factStore.read({
169
+ partitions: query.partitions || (query.partition ? [query.partition] : undefined),
170
+ targetKey: query.targetKey ?? query.target,
171
+ runtimeEpoch: query.runtimeEpoch,
172
+ cursor: query.cursor,
173
+ limit: query.limit,
174
+ ...(Object.prototype.hasOwnProperty.call(query, 'actionId')
175
+ ? { actionId: query.actionId }
176
+ : {}),
177
+ });
178
+ },
179
+ status() {
180
+ return factStore.status();
181
+ },
182
+ close() {},
183
+ };
184
+ }
185
+
186
+ function partitionFor(kind) {
187
+ if (kind === 'observation' || kind === 'summary') return 'ui';
188
+ return 'action';
189
+ }
190
+
191
+ function createFileEvidenceAdapter({ dir } = {}) {
192
+ if (!dir) throw new TypeError('dir is required');
193
+ fs.mkdirSync(dir, { recursive: true });
194
+ const indexPath = path.join(dir, 'index.json');
195
+
196
+ function loadIndex() {
197
+ if (!fs.existsSync(indexPath)) return [];
198
+ return JSON.parse(fs.readFileSync(indexPath, 'utf8'));
199
+ }
200
+
201
+ function writeAtomic(file, data) {
202
+ const tmp = `${file}.${process.pid}.tmp`;
203
+ fs.writeFileSync(tmp, data);
204
+ fs.renameSync(tmp, file);
205
+ }
206
+
207
+ function saveIndex(ids) {
208
+ writeAtomic(indexPath, JSON.stringify(ids));
209
+ }
210
+
211
+ function fileFor(evidenceId) {
212
+ return path.join(dir, `${encodeURIComponent(evidenceId)}.json`);
213
+ }
214
+
215
+ const adapter = {
216
+ record(envelope) {
217
+ writeAtomic(fileFor(envelope.evidenceId), JSON.stringify(envelope));
218
+ const ids = loadIndex();
219
+ ids.push(envelope.evidenceId);
220
+ saveIndex(ids);
221
+ return { ok: true, stored: true, evidenceId: envelope.evidenceId, globalSeq: ids.length };
222
+ },
223
+ offer(envelope) {
224
+ return { accepted: true, completion: Promise.resolve().then(() => adapter.record(envelope)) };
225
+ },
226
+ readById(evidenceId) {
227
+ const file = fileFor(evidenceId);
228
+ if (!fs.existsSync(file)) return null;
229
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
230
+ },
231
+ list({ namespace, operationId } = {}) {
232
+ return loadIndex()
233
+ .map((id) => this.readById(id))
234
+ .filter((item) => item
235
+ && (!namespace || item.namespace === namespace)
236
+ && (!operationId || item.operationId === operationId));
237
+ },
238
+ status() {
239
+ return { ok: true, adapter: 'file', dir, count: loadIndex().length };
240
+ },
241
+ close() {},
242
+ };
243
+ return adapter;
244
+ }
245
+
246
+ module.exports = {
247
+ createLegacyFactStoreAdapter,
248
+ createMemoryEvidenceAdapter,
249
+ createSegmentedEvidenceAdapter,
250
+ createFileEvidenceAdapter,
251
+ };
@@ -0,0 +1,329 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const crypto = require('node:crypto');
6
+ const { checksumOf, canonicalJson, validateArchivedRecord, verifyChecksum } = require('./evidence-schema');
7
+ const { analyzeRecordedPayloads } = require('./recorded-payload-archive');
8
+ const { readRegularFile: readRecordedFile } = require('./evidence-recording');
9
+
10
+ const SCHEMA = 'aab.evidence-archive/v1';
11
+ const PAYLOAD_ARCHIVE_SCHEMA = 'aab.evidence-archive/v2';
12
+ const MAX_RECORDS = 10_000;
13
+ const MAX_BYTES = 128 * 1024 * 1024;
14
+ const MAX_MANIFEST_BYTES = 2 * 1024 * 1024;
15
+ const sha256 = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
16
+
17
+ function requireValue(condition, code, detail) {
18
+ if (!condition) throw Object.assign(new Error(code), { code, detail });
19
+ }
20
+
21
+ function textArgument(value, field) {
22
+ requireValue(typeof value === 'string' && value.trim().length > 0, 'invalid_argument', field);
23
+ }
24
+
25
+ function namespaceArgument(namespace) {
26
+ requireValue(namespace === 'intent' || namespace === 'script', 'invalid_argument', 'namespace');
27
+ }
28
+
29
+ async function handle(args = {}, { getFactStore } = {}) {
30
+ try {
31
+ if (args.operation === 'verify') return verifyArchive(args);
32
+ if (args.operation === 'export') {
33
+ namespaceArgument(args.namespace);
34
+ textArgument(args.operationId, 'operationId');
35
+ textArgument(args.outputDir, 'outputDir');
36
+ requireValue(args.includeRecordedPayloads === undefined || typeof args.includeRecordedPayloads === 'boolean',
37
+ 'invalid_argument', 'includeRecordedPayloads');
38
+ requireValue(!fs.existsSync(path.resolve(args.outputDir)), 'output_exists');
39
+ const store = getFactStore();
40
+ await store.drain();
41
+ return exportArchive(args, store);
42
+ }
43
+ return { ok: false, error: 'invalid_operation' };
44
+ } catch (error) {
45
+ return {
46
+ ok: false,
47
+ error: error.code || 'archive_failed',
48
+ ...(error.detail === undefined ? {} : { detail: error.detail }),
49
+ ...(error.code === 'invalid_argument' ? { field: error.detail } : {}),
50
+ };
51
+ }
52
+ }
53
+
54
+ // There is no await between the watermark, page reads and final status: the
55
+ // owning Host writer cannot interleave another command with this snapshot.
56
+ function exportArchive({ namespace, operationId, outputDir, includeRecordedPayloads = false }, store) {
57
+ const status = store.status();
58
+ requireValue(status.ok === true && status.authoritative === true && status.persistence === true,
59
+ 'fact_store_unavailable');
60
+ requireValue(Number.isSafeInteger(status.sequences?.nextGlobalSeq), 'invalid_store_snapshot');
61
+ const throughGlobalSeq = status.sequences.nextGlobalSeq - 1;
62
+ requireValue(typeof status.storeId === 'string' && status.storeId.length > 0 && Number.isSafeInteger(throughGlobalSeq)
63
+ && throughGlobalSeq >= 0, 'invalid_store_snapshot');
64
+ const targetKey = `evidence:${namespace}:${operationId}`;
65
+ const facts = [];
66
+ const cursors = new Set();
67
+ let cursor;
68
+ let byteCount = 2;
69
+ do {
70
+ const page = store.read({ targetKey, limit: 1000, ...(cursor === undefined ? {} : { cursor }) });
71
+ requireValue(page.ok === true, 'fact_store_read_failed', page.error);
72
+ requireValue(page.gap === false && page.cursorExpired === false, 'evidence_gap');
73
+ requireValue(Array.isArray(page.items) && typeof page.hasMore === 'boolean', 'invalid_store_page');
74
+ for (const fact of page.items) {
75
+ byteCount += Buffer.byteLength(JSON.stringify(fact)) + 1;
76
+ requireValue(facts.length < MAX_RECORDS && byteCount <= MAX_BYTES, 'archive_limit_exceeded');
77
+ facts.push(fact);
78
+ }
79
+ if (!page.hasMore) break;
80
+ requireValue(page.items.length > 0 && typeof page.cursor === 'string' && page.cursor.length > 0 && !cursors.has(page.cursor),
81
+ 'invalid_store_cursor');
82
+ cursors.add(page.cursor);
83
+ cursor = page.cursor;
84
+ } while (true);
85
+ requireValue(facts.length > 0, 'operation_not_found');
86
+ const after = store.status();
87
+ requireValue(after.ok === true && after.storeId === status.storeId
88
+ && after.sequences?.nextGlobalSeq === throughGlobalSeq + 1, 'store_changed_during_export');
89
+ const analysis = analyzeFacts(facts, { namespace, operationId, throughGlobalSeq });
90
+ const payloadFiles = new Map();
91
+ if (includeRecordedPayloads) {
92
+ includePayloadAnalysis(analysis, analyzeRecordedPayloads(facts, (record, file, name) => {
93
+ const bytes = readRecordedFile(record.directory, file.name);
94
+ payloadFiles.set(name, bytes);
95
+ return bytes;
96
+ }));
97
+ }
98
+ const recordsBytes = Buffer.from(JSON.stringify(facts) + '\n');
99
+ requireValue(recordsBytes.length <= MAX_BYTES, 'archive_limit_exceeded');
100
+ const manifest = {
101
+ schemaVersion: includeRecordedPayloads ? PAYLOAD_ARCHIVE_SCHEMA : SCHEMA,
102
+ namespace,
103
+ operationId,
104
+ exportedAtMs: Date.now(),
105
+ source: {
106
+ kind: 'host-fact-store',
107
+ storeId: status.storeId,
108
+ targetKey,
109
+ throughGlobalSeq,
110
+ // These describe retention for whole partitions, not just this operation.
111
+ retention: { ui: status.quota?.partitions?.ui ?? null, action: status.quota?.partitions?.action ?? null },
112
+ },
113
+ records: { path: 'records.json', bytes: recordsBytes.length, sha256: sha256(recordsBytes) },
114
+ ...analysis,
115
+ };
116
+ const manifestBytes = Buffer.from(JSON.stringify(manifest, null, 2) + '\n');
117
+ requireValue(manifestBytes.length <= MAX_MANIFEST_BYTES, 'archive_limit_exceeded');
118
+ const directory = path.resolve(outputDir);
119
+ // An output directory is created exclusively. Interrupted writes have no
120
+ // manifest, and existing user files are never replaced or removed.
121
+ try { fs.mkdirSync(directory); }
122
+ catch (error) { if (error.code === 'EEXIST') error.code = 'output_exists'; throw error; }
123
+ fs.writeFileSync(path.join(directory, 'records.json'), recordsBytes, { flag: 'wx' });
124
+ for (const [name, bytes] of payloadFiles) fs.writeFileSync(path.join(directory, name), bytes, { flag: 'wx', mode: 0o600 });
125
+ fs.writeFileSync(path.join(directory, 'manifest.json'), manifestBytes, { flag: 'wx' });
126
+ return resultOf(manifest, directory, sha256(manifestBytes));
127
+ }
128
+
129
+ function analyzeFacts(facts, { namespace, operationId, throughGlobalSeq }) {
130
+ requireValue(Array.isArray(facts) && facts.length > 0 && facts.length <= MAX_RECORDS, 'invalid_records');
131
+ const ids = new Map();
132
+ const positions = new Map();
133
+ const counts = {};
134
+ const targets = new Map();
135
+ let previousSequence = 0;
136
+ function target(role, value) {
137
+ if (value === undefined) return;
138
+ requireValue((value === null && namespace === 'script')
139
+ || (value !== null && typeof value === 'object' && !Array.isArray(value)), 'invalid_record_target');
140
+ const item = { role, value };
141
+ targets.set(canonicalJson(item), item);
142
+ }
143
+ for (const fact of facts) {
144
+ requireValue(fact && Number.isSafeInteger(fact.globalSeq) && fact.globalSeq > previousSequence
145
+ && fact.globalSeq <= throughGlobalSeq, 'invalid_record_sequence');
146
+ previousSequence = fact.globalSeq;
147
+ const record = fact.payload;
148
+ requireValue(record?.namespace === namespace && record.operationId === operationId, 'record_scope_mismatch');
149
+ requireValue(fact.targetKey === `evidence:${namespace}:${operationId}`
150
+ && fact.runtimeEpoch === operationId && fact.actionId === record.evidenceId, 'fact_binding_mismatch');
151
+ requireValue(record.persisted === true && typeof record.evidenceId === 'string' && record.evidenceId.length > 0,
152
+ 'invalid_evidence_record');
153
+ requireValue(!ids.has(record.evidenceId), 'duplicate_evidence_id');
154
+ requireValue(validateArchivedRecord(namespace, record.kind, record).ok, 'invalid_evidence_record');
155
+ requireValue(verifyChecksum(record).ok, 'record_checksum_mismatch', record.evidenceId);
156
+ ids.set(record.evidenceId, record);
157
+ positions.set(record.evidenceId, fact.globalSeq);
158
+ counts[record.kind] = (counts[record.kind] || 0) + 1;
159
+ target(namespace === 'script' && record.kind === 'dispatch-marker' ? 'dispatch' : 'owner', record.target);
160
+ target('owner', record.requestedTarget);
161
+ target('foreground', record.foreground);
162
+ if (record.kind === 'observation') {
163
+ if (record.schemaVersion === undefined) target('observed', { serial: record.serial, packageName: record.packageName });
164
+ else target('observed', record.observedTarget);
165
+ }
166
+ }
167
+ const missingReferences = [];
168
+ const externalReferences = [];
169
+ function uniqueIndex(kind, field) {
170
+ const index = new Map();
171
+ for (const { payload: record } of facts) {
172
+ if (record.kind !== kind || record[field] === undefined) continue;
173
+ requireValue(!index.has(record[field]), 'ambiguous_evidence_reference', field);
174
+ index.set(record[field], record);
175
+ }
176
+ return index;
177
+ }
178
+ const observations = uniqueIndex('observation', 'rawTreeId');
179
+ const summaries = new Set(facts.filter(f => f.payload.kind === 'summary').map(f => f.payload.rawTreeId));
180
+ const markers = uniqueIndex('dispatch-marker', 'actionId');
181
+ const decisions = uniqueIndex('decision', 'decisionId');
182
+ const missing = (record, field, value) => missingReferences.push({ evidenceId: record.evidenceId, field, value });
183
+ const precedes = (reference, record) => requireValue(
184
+ positions.get(reference.evidenceId) < positions.get(record.evidenceId), 'invalid_reference_binding', record.evidenceId);
185
+ for (const { payload: record } of facts) {
186
+ if (record.kind === 'observation' && record.rawTree == null) missing(record, 'rawTree', null);
187
+ if (record.kind === 'observation' && !summaries.has(record.rawTreeId)) missing(record, 'summary', record.rawTreeId ?? null);
188
+ if (record.basedOnEvidenceIds !== undefined) {
189
+ requireValue(Array.isArray(record.basedOnEvidenceIds), 'invalid_evidence_references');
190
+ for (const id of record.basedOnEvidenceIds) {
191
+ if (!ids.has(id)) missing(record, 'basedOnEvidenceIds', id);
192
+ else precedes(ids.get(id), record);
193
+ }
194
+ }
195
+ if (record.parentFactId) {
196
+ if (!ids.has(record.parentFactId)) missing(record, 'parentFactId', record.parentFactId);
197
+ else precedes(ids.get(record.parentFactId), record);
198
+ }
199
+ if (record.kind !== 'observation' && record.rawTreeId !== undefined) {
200
+ const observation = observations.get(record.rawTreeId);
201
+ if (!observation) missing(record, 'rawTreeId', record.rawTreeId);
202
+ else {
203
+ precedes(observation, record);
204
+ requireValue(observation.revision === record.revision, 'invalid_reference_binding', record.evidenceId);
205
+ }
206
+ }
207
+ if (record.kind === 'dispatch-marker' && record.decisionId) {
208
+ const decision = decisions.get(record.decisionId);
209
+ if (!decision) missing(record, 'decisionId', record.decisionId);
210
+ else {
211
+ precedes(decision, record);
212
+ requireValue(decision.revision === record.revision && decision.actionSpecHash === record.actionSpecHash,
213
+ 'invalid_reference_binding', record.evidenceId);
214
+ }
215
+ }
216
+ if (record.kind === 'checkpoint' && record.resultRef) {
217
+ const ref = record.resultRef, result = ids.get(ref.evidenceId);
218
+ if (!result) missing(record, 'resultRef', ref.evidenceId);
219
+ else {
220
+ precedes(result, record);
221
+ requireValue(result.kind === 'result' && result.revision === record.revision
222
+ && result.bytes === ref.bytes && result.sha256 === ref.sha256 && checksumOf(result.result) === ref.sha256
223
+ && Buffer.byteLength(canonicalJson(result.result)) === ref.bytes
224
+ && result.originalSha256 === ref.originalSha256 && result.representation === ref.representation,
225
+ 'invalid_reference_binding', record.evidenceId);
226
+ }
227
+ }
228
+ if (record.kind === 'action-receipt') {
229
+ const marker = markers.get(record.actionId);
230
+ if (!marker) missing(record, 'actionId', record.actionId);
231
+ else {
232
+ precedes(marker, record);
233
+ requireValue(record.target === undefined || canonicalJson(record.target) === canonicalJson(marker.target),
234
+ 'invalid_reference_binding', record.evidenceId);
235
+ }
236
+ }
237
+ for (const field of ['captureRefs', 'evidenceRefs']) {
238
+ if (record[field] === undefined) continue;
239
+ requireValue(Array.isArray(record[field]), 'invalid_evidence_references');
240
+ for (const ref of record[field]) {
241
+ if (typeof ref !== 'string' || !ids.has(ref)) externalReferences.push({ evidenceId: record.evidenceId, field, ref });
242
+ }
243
+ }
244
+ if (record.kind === 'attachment') externalReferences.push({ evidenceId: record.evidenceId,
245
+ field: 'recordedPayload', ref: { recordingId: record.recordingId, sequence: record.sequence, file: record.file } });
246
+ }
247
+ return {
248
+ recordCount: facts.length,
249
+ firstGlobalSeq: facts[0].globalSeq,
250
+ lastGlobalSeq: facts.at(-1).globalSeq,
251
+ counts,
252
+ targets: [...targets.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, value]) => value),
253
+ coverage: {
254
+ scope: 'retained-host-records',
255
+ priorHistoryComplete: 'unknown',
256
+ referenceClosure: missingReferences.length === 0 ? 'complete' : 'partial',
257
+ missingReferences,
258
+ externalPayloads: 'not-included',
259
+ externalReferences,
260
+ executionStatus: 'not-inferred',
261
+ businessVerdict: 'not-evaluated',
262
+ exclusions: ['external-screenshots', 'mobile-capture-items', 'intermediate-script-call-results-and-assertions', 'in-memory-events'],
263
+ },
264
+ };
265
+ }
266
+
267
+ function readRegularFile(directory, name, maxBytes) {
268
+ const file = path.join(directory, name);
269
+ const stat = fs.lstatSync(file);
270
+ requireValue(stat.isFile() && !stat.isSymbolicLink(), 'invalid_archive_file', name);
271
+ requireValue(stat.size <= maxBytes, 'archive_limit_exceeded');
272
+ const descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
273
+ try {
274
+ const opened = fs.fstatSync(descriptor);
275
+ requireValue(opened.isFile() && opened.size <= maxBytes, 'invalid_archive_file', name);
276
+ const bytes = fs.readFileSync(descriptor);
277
+ requireValue(bytes.length <= maxBytes, 'archive_limit_exceeded');
278
+ return bytes;
279
+ } finally { fs.closeSync(descriptor); }
280
+ }
281
+
282
+ function verifyArchive({ archiveDir, manifestSha256 }) {
283
+ textArgument(archiveDir, 'archiveDir');
284
+ requireValue(typeof manifestSha256 === 'string' && /^[a-f0-9]{64}$/.test(manifestSha256),
285
+ 'invalid_argument', 'manifestSha256');
286
+ const directory = path.resolve(archiveDir);
287
+ requireValue(fs.lstatSync(directory).isDirectory() && !fs.lstatSync(directory).isSymbolicLink(), 'invalid_archive_directory');
288
+ const manifestBytes = readRegularFile(directory, 'manifest.json', MAX_MANIFEST_BYTES);
289
+ requireValue(sha256(manifestBytes) === manifestSha256, 'manifest_checksum_mismatch');
290
+ const manifest = JSON.parse(manifestBytes);
291
+ requireValue([SCHEMA, PAYLOAD_ARCHIVE_SCHEMA].includes(manifest.schemaVersion), 'unsupported_archive_schema');
292
+ namespaceArgument(manifest.namespace);
293
+ textArgument(manifest.operationId, 'operationId');
294
+ requireValue(manifest.records?.path === 'records.json', 'invalid_archive_path');
295
+ requireValue(manifest.source?.kind === 'host-fact-store' && typeof manifest.source.storeId === 'string'
296
+ && manifest.source.targetKey === `evidence:${manifest.namespace}:${manifest.operationId}`
297
+ && Number.isSafeInteger(manifest.source.throughGlobalSeq) && manifest.source.throughGlobalSeq >= 0,
298
+ 'invalid_archive_source');
299
+ const recordsBytes = readRegularFile(directory, 'records.json', MAX_BYTES);
300
+ requireValue(recordsBytes.length === manifest.records.bytes && sha256(recordsBytes) === manifest.records.sha256,
301
+ 'records_checksum_mismatch');
302
+ const facts = JSON.parse(recordsBytes);
303
+ const analysis = analyzeFacts(facts, { namespace: manifest.namespace,
304
+ operationId: manifest.operationId, throughGlobalSeq: manifest.source.throughGlobalSeq });
305
+ if (manifest.schemaVersion === PAYLOAD_ARCHIVE_SCHEMA) {
306
+ includePayloadAnalysis(analysis, analyzeRecordedPayloads(facts, (_record, _file, name) => readRecordedFile(directory, name)));
307
+ } else requireValue(manifest.recordedPayloads === undefined, 'manifest_analysis_mismatch', 'recordedPayloads');
308
+ for (const [field, expected] of Object.entries(analysis)) {
309
+ requireValue(canonicalJson(manifest[field]) === canonicalJson(expected), 'manifest_analysis_mismatch', field);
310
+ }
311
+ return { ...resultOf(manifest, directory, manifestSha256), integrity: 'verified' };
312
+ }
313
+
314
+ function includePayloadAnalysis(analysis, payloads) {
315
+ analysis.recordedPayloads = payloads;
316
+ analysis.coverage.externalPayloads = 'included-retained-recordings';
317
+ analysis.coverage.exclusions = payloads.exclusions;
318
+ analysis.coverage.missingReferences.push(...payloads.missingReferences);
319
+ analysis.coverage.referenceClosure = analysis.coverage.missingReferences.length ? 'partial' : 'complete';
320
+ }
321
+
322
+ function resultOf(manifest, directory, manifestSha256) {
323
+ return { ok: true, archiveDir: directory, manifestPath: path.join(directory, 'manifest.json'), manifestSha256,
324
+ namespace: manifest.namespace, operationId: manifest.operationId, recordCount: manifest.recordCount,
325
+ targets: manifest.targets, coverage: manifest.coverage,
326
+ ...(manifest.recordedPayloads ? { recordedPayloads: manifest.recordedPayloads } : {}) };
327
+ }
328
+
329
+ module.exports = { handle, analyzeFacts };