@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,60 @@
1
+ 'use strict';
2
+
3
+ const { randomUUID } = require('node:crypto');
4
+ const { isDeepStrictEqual } = require('node:util');
5
+ const { executionSleep } = require('./execution-scope');
6
+ const { validateValue } = require('./argument-schema');
7
+ const h5 = require('./flutter-h5-target');
8
+ const reject = error => ({ ok: false, error, dispatched: false, ambiguous: false });
9
+
10
+ // The same managed Flutter action port owns admission, receipts and cancellation.
11
+ // No arbitrary JavaScript or active-adapter lookup is used by typed commands.
12
+ function createFlutterH5Port(invoke, { actionId } = {}) {
13
+ const mutate = payload => invoke(payload, { actionId });
14
+ async function snapshot(adapterId) {
15
+ const result = await invoke({ action: 'h5Dom', ...(adapterId === undefined ? {} : { adapterId }) }, { actionId: randomUUID() });
16
+ if (result.ok !== true) return result;
17
+ if (result.h5TargetSchema !== h5.schema) return reject('flutter_h5_target_schema_required');
18
+ try { validateValue(result.pageRef, h5.pageSchema, 'pageRef'); }
19
+ catch { return reject('invalid_flutter_h5_page_ref'); }
20
+ if (!Array.isArray(result.dom?.controls) || typeof result.dom.truncated !== 'boolean'
21
+ || result.dom.documentId !== result.pageRef.documentId || result.dom.url !== result.pageRef.url) return reject('invalid_flutter_h5_snapshot');
22
+ if (adapterId !== undefined && result.pageRef.adapterId !== adapterId) return reject('reobserve_required');
23
+ return result;
24
+ }
25
+ async function control(operation, options) {
26
+ const expected = options.expectedTarget?.pageRef ?? options.expectedPage;
27
+ if (options.adapterId !== undefined && expected && options.adapterId !== expected.adapterId) return reject('flutter_h5_adapter_conflict');
28
+ const tree = await snapshot(options.adapterId ?? expected?.adapterId);
29
+ if (tree.ok !== true) return tree;
30
+ if (operation === 'scrollBy') {
31
+ if (expected !== undefined && !isDeepStrictEqual(expected, tree.pageRef)) return reject('reobserve_required');
32
+ return mutate({ action: 'h5Control', operation, expectedPage: tree.pageRef,
33
+ deltaX: options.deltaX, deltaY: options.deltaY });
34
+ }
35
+ const selected = h5.selectH5Node(tree, options.selector, options.expectedTarget);
36
+ if (!selected.ok) return selected;
37
+ return mutate({ action: 'h5Control', operation, expectedTarget: selected.targetRef,
38
+ ...(operation === 'input' ? { text: options.text } : {}) });
39
+ }
40
+ async function wait(options) {
41
+ const timeoutMs = options.timeoutMs ?? 5000, intervalMs = options.intervalMs ?? 250;
42
+ const deadline = Date.now() + timeoutMs;
43
+ let page, selected;
44
+ do {
45
+ const tree = await snapshot(page?.adapterId ?? options.adapterId);
46
+ if (tree.ok !== true) return tree;
47
+ if (page && !isDeepStrictEqual(page, tree.pageRef)) return reject('reobserve_required');
48
+ page = tree.pageRef;
49
+ selected = h5.selectH5Node(tree, options.selector);
50
+ if (selected.ok || selected.error !== 'flutter_h5_selector_not_found') return selected;
51
+ const remaining = deadline - Date.now();
52
+ if (remaining <= 0) break;
53
+ await executionSleep(Math.min(intervalMs, remaining));
54
+ } while (Date.now() < deadline);
55
+ return { ...reject('flutter_h5_wait_timeout'), timeoutMs, lastResult: selected };
56
+ }
57
+ return { snapshot, control, wait,
58
+ evaluate: options => mutate({ action: 'h5Eval', script: options.script, expectedPage: options.expectedPage }) };
59
+ }
60
+ module.exports = { createFlutterH5Port };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ const { object, text } = require('./argument-schema');
4
+ const { h5TargetContract } = require('./h5-target');
5
+ const schema = 'aab.flutter-h5-target/v1';
6
+ const fields = ['runtimeEpoch', 'adapterId', 'adapterGeneration', 'documentId', 'url'];
7
+ const pageSchema = object({ schemaVersion: { const: schema },
8
+ ...Object.fromEntries(fields.map(key => [key, text])) }, ['schemaVersion', ...fields]);
9
+ module.exports = h5TargetContract({ schema, pageSchema, errorPrefix: 'flutter_h5' });
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+
3
+ const { validBounds } = require('./native-target');
4
+
5
+ function selectFlutterNode(rawTree, spec) {
6
+ const reject = error => ({ ok: false, error, dispatched: false });
7
+ if (!Array.isArray(rawTree?.nodes) || rawTree.ok === false) return reject('observed_flutter_tree_required');
8
+ if (rawTree.truncated === true) return reject('flutter_observation_incomplete');
9
+ const selector = spec.selector || (typeof spec.text === 'string' ? { text: spec.text } : null);
10
+ const keys = Object.keys(selector || {});
11
+ if (keys.length !== 1 || !['text', 'nodeId'].includes(keys[0])
12
+ || typeof selector[keys[0]] !== 'string' || !selector[keys[0]]
13
+ || spec.exact === false || (spec.selector && spec.text != null)) return reject('explicit_exact_flutter_selector_required');
14
+ const action = spec.action === 'inputText' ? 'input' : ['scrollBy', 'scrollUntilText'].includes(spec.action) ? 'scroll' : 'tap';
15
+ const operable = node => node.enabled !== false && node.visible !== false && node.offstage !== true && validBounds(node[action]?.bounds);
16
+ const matches = rawTree.nodes.filter(node => keys[0] === 'nodeId'
17
+ ? node.id != null && String(node.id) === selector.nodeId
18
+ : (node.text === selector.text || node.value === selector.text) && operable(node));
19
+ if (matches.length !== 1) return reject(matches.length ? 'flutter_selector_not_unique' : 'flutter_selector_not_found');
20
+ const node = matches[0];
21
+ if (!operable(node)) return reject('flutter_node_not_operable');
22
+ const bounds = node[action].bounds;
23
+ const x = (bounds.left + bounds.right) / 2, y = (bounds.top + bounds.bottom) / 2;
24
+ const viewport = rawTree.viewport;
25
+ if (x < 0 || y < 0 || (Number.isFinite(viewport?.logicalWidth) && x >= viewport.logicalWidth)
26
+ || (Number.isFinite(viewport?.logicalHeight) && y >= viewport.logicalHeight)) return reject('flutter_node_not_operable');
27
+ return { ok: true, node, x, y };
28
+ }
29
+
30
+ function flutterNodeIdentity(node) {
31
+ return Object.fromEntries(['id', 'text', 'value', 'label', 'hint', 'widgetType', 'role', 'targetRef'].map(key => [key, node?.[key] ?? null]));
32
+ }
33
+
34
+ function flutterTargetRequest(node, selector) {
35
+ const ref = node?.targetRef;
36
+ if (!ref || ref.schemaVersion !== 'aab.flutter-target/v1'
37
+ || !['runtimeEpoch', 'elementId', 'guard'].every(key => typeof ref[key] === 'string' && ref[key])) {
38
+ return { ok: false, error: 'flutter_atomic_target_unavailable', dispatched: false, ambiguous: false,
39
+ message: 'Update the Flutter SDK and observe again. This action requires a live Element reference.' };
40
+ }
41
+ return { ok: true, request: { selector, targetRef: ref } };
42
+ }
43
+
44
+ function bindFlutterAction(rawTree, payload) {
45
+ const reject = error => ({ ok: false, error, dispatched: false, ambiguous: false });
46
+ if (rawTree?.ok === false || !Array.isArray(rawTree?.nodes) || rawTree.truncated === true) return reject('flutter_observation_incomplete');
47
+ let selector = payload.selector;
48
+ if (payload.action === 'tapText') selector = { text: payload.text };
49
+ if (!selector) {
50
+ const action = payload.action === 'inputText' ? 'input' : 'scroll';
51
+ let nodes = rawTree.nodes.filter(node => validBounds(node[action]?.bounds) && node.enabled !== false && node.visible !== false);
52
+ if (action === 'input') {
53
+ if (payload.x !== undefined || payload.y !== undefined) {
54
+ if (!Number.isFinite(payload.x) || !Number.isFinite(payload.y)) return reject('invalid_flutter_coordinates');
55
+ nodes = nodes.filter(node => { const b = node.input.bounds; return payload.x >= b.left && payload.x < b.right && payload.y >= b.top && payload.y < b.bottom; });
56
+ } else {
57
+ const focused = nodes.filter(node => node.input.focused === true);
58
+ if (focused.length) nodes = focused;
59
+ }
60
+ } else nodes = nodes.filter(node => node.role === 'scrollable');
61
+ if (nodes.length !== 1) return reject(nodes.length ? 'flutter_selector_not_unique' : 'flutter_selector_not_found');
62
+ selector = { nodeId: String(nodes[0].id) };
63
+ }
64
+ const selected = selectFlutterNode(rawTree, { action: payload.action, selector });
65
+ if (!selected.ok) return selected;
66
+ const guarded = flutterTargetRequest(selected.node, selector);
67
+ if (!guarded.ok) return guarded;
68
+ const { x, y, ...action } = payload;
69
+ if (payload.action === 'tapText') delete action.text;
70
+ return { ok: true, payload: {
71
+ ...action, ...(payload.action === 'tapText' ? { action: 'tapTarget' } : {}), ...guarded.request,
72
+ } };
73
+ }
74
+
75
+ module.exports = { selectFlutterNode, flutterNodeIdentity, flutterTargetRequest, bindFlutterAction };
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ const managed = require('./managed-sdk-execution');
4
+ const schema = 'aab.h5-execution/v1';
5
+
6
+ module.exports = {
7
+ schema,
8
+ executeH5Action: options => managed.executeManagedAction({ ...options, kind: 'h5', schema }),
9
+ terminalReceipt: (result, identity) => managed.terminalReceipt(schema, result, identity),
10
+ settlementProof: (result, identity) => managed.settlementProof('h5', schema, result, identity),
11
+ };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const { isDeepStrictEqual } = require('node:util');
4
+ const { object, text } = require('./argument-schema');
5
+ const string = { type: 'string' };
6
+ const elementKeys = ['elementId', 'tag', 'id', 'name', 'type', 'text', 'ariaLabel', 'href'];
7
+ const elementSchema = object(Object.fromEntries(elementKeys.map(key => [key,
8
+ key === 'elementId' ? text : string])), elementKeys);
9
+ const selectorSchema = { ...object({ elementId: text, text, ariaLabel: text, tag: text }),
10
+ oneOf: ['elementId', 'text', 'ariaLabel'].map(key => ({ required: [key] })) };
11
+
12
+ function h5TargetContract({ schema, pageSchema, errorPrefix }) {
13
+ const expectedTargetSchema = object({ pageRef: pageSchema, element: elementSchema }, ['pageRef', 'element']);
14
+ const reject = error => ({ ok: false, error, dispatched: false, ambiguous: false });
15
+ function selectH5Node(tree, selector, expectedTarget) {
16
+ if (tree?.h5TargetSchema !== schema) return reject(errorPrefix + '_target_schema_required');
17
+ if (tree.dom.truncated && selector.elementId === undefined) return reject(errorPrefix + '_snapshot_truncated');
18
+ const nodes = tree.dom.controls.filter(node => node.visible === true
19
+ && ['elementId', 'text', 'ariaLabel', 'tag'].every(key => selector[key] === undefined || selector[key] === node[key]));
20
+ if (nodes.length !== 1) return { ...reject(errorPrefix + (nodes.length ? '_selector_ambiguous' : '_selector_not_found')),
21
+ matched: nodes.length };
22
+ const node = nodes[0];
23
+ if (node.disabled) return reject(errorPrefix + '_target_disabled');
24
+ const targetRef = { pageRef: tree.pageRef, element: Object.fromEntries(elementKeys.map(key => [key, node[key]])) };
25
+ if (expectedTarget !== undefined && !isDeepStrictEqual(expectedTarget, targetRef)) return reject('reobserve_required');
26
+ return { ok: true, node, targetRef };
27
+ }
28
+ return { schema, selectorSchema, pageSchema, expectedTargetSchema, selectH5Node };
29
+ }
30
+
31
+ module.exports = { h5TargetContract };
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+ const { createFactStore } = require('../fact-store');
5
+ const { defaultSegmentedFactStorePath } = require('../segmented-fact-store');
6
+ const { CommandError } = require('../command-errors');
7
+ const { canonicalPath } = require('./canonical-path');
8
+
9
+ let shared;
10
+ let exitInstalled = false;
11
+
12
+ function hostFactStoreTarget() {
13
+ const explicit = process.env.AI_APP_BRIDGE_FACT_STORE_DIR, legacy = process.env.AI_APP_BRIDGE_FACT_CACHE_PATH;
14
+ const profile = process.env.AI_APP_BRIDGE_FACT_CACHE_PROFILE || 'auto';
15
+ if (!['auto', '64mb', '256mb', '512mb', '1gb'].includes(profile))
16
+ throw new CommandError('invalid_fact_store_profile', 'FactStore profile must be auto, 64mb, 256mb, 512mb or 1gb.');
17
+ return { directory: canonicalPath(explicit || (legacy ? path.join(path.dirname(path.resolve(legacy)), 'fact-store-v1') : defaultSegmentedFactStorePath())),
18
+ profile };
19
+ }
20
+
21
+ function open(target) {
22
+ try { return createFactStore(target); }
23
+ catch (error) {
24
+ if (error.code === 'sfs_busy') throw new CommandError('fact_store_writer_busy',
25
+ 'Another Host owns this FactStore. Reconcile from that Host, or close it before retrying. Original device acknowledgement remains pending.');
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ function getHostFactStore() {
31
+ if (!shared || shared.closed) shared = open(hostFactStoreTarget());
32
+ if (!exitInstalled) {
33
+ exitInstalled = true;
34
+ process.once('exit', () => { try { shared?.close(); } catch { /* Process exit retains committed facts. */ } });
35
+ }
36
+ return shared;
37
+ }
38
+
39
+ function closeHostFactStore() { shared?.close(); shared = null; }
40
+
41
+ function withHostFactStore(target, action) {
42
+ if (canonicalPath(target.directory) === hostFactStoreTarget().directory) return action(getHostFactStore());
43
+ // Recovery follows the original journal's store destination, even when this
44
+ // Host was opened with a different recording directory. Close this bounded handle.
45
+ const store = open(target);
46
+ try { return action(store); } finally { store.close(); }
47
+ }
48
+
49
+ module.exports = { hostFactStoreTarget, getHostFactStore, closeHostFactStore, withHostFactStore };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ const { object, text, integer } = require('./argument-schema');
4
+ const { h5TargetContract } = require('./h5-target');
5
+ const schema = 'aab.ios-h5-target/v1';
6
+ const pageSchema = object({ schemaVersion: { const: schema }, runtimeEpoch: text, bundleId: text,
7
+ processId: integer(1), webViewId: text, documentId: text, url: text },
8
+ ['schemaVersion', 'runtimeEpoch', 'bundleId', 'processId', 'webViewId', 'documentId', 'url']);
9
+ module.exports = h5TargetContract({ schema, pageSchema, errorPrefix: 'ios_h5' });
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const { isDeepStrictEqual } = require('node:util');
4
+ const { object, text, integer } = require('./argument-schema');
5
+
6
+ const schema = 'aab.ios-native-target/v1';
7
+ const sessionSchema = 'aab.ios-native-session/v1';
8
+ const orientationSchema = { enum: ['portrait', 'landscapeLeft', 'landscapeRight', 'portraitUpsideDown'],
9
+ description: 'Requested App interface orientation. Landscape directions are UIInterfaceOrientation, not the opposite UIDeviceOrientation.' };
10
+ const expectedSessionSchema = object({ schemaVersion: { const: sessionSchema }, runnerEpoch: text,
11
+ bundleId: text, processId: integer(1), sessionId: text },
12
+ ['schemaVersion', 'runnerEpoch', 'bundleId', 'processId', 'sessionId']);
13
+ const nullableLabel = { anyOf: [{ type: 'string', maxLength: 16384 }, { type: 'null' }] };
14
+ const selectorSchema = { ...object({ accessibilityId: text, label: text, elementId: text, type: text }),
15
+ oneOf: ['accessibilityId', 'label', 'elementId'].map(key => ({ required: [key] })) };
16
+ const elementSchema = object({ elementId: text, type: text, identifier: nullableLabel, label: nullableLabel },
17
+ ['elementId', 'type', 'identifier', 'label']);
18
+ const expectedTargetSchema = object({ schemaVersion: { const: schema }, runnerEpoch: text,
19
+ bundleId: text, processId: integer(1), sessionId: text, element: elementSchema },
20
+ ['schemaVersion', 'runnerEpoch', 'bundleId', 'processId', 'sessionId', 'element']);
21
+ const reject = error => ({ ok: false, error, dispatched: false, ambiguous: false });
22
+
23
+ function nodesOf(tree) {
24
+ const nodes = [];
25
+ function walk(node) {
26
+ if (!node || typeof node !== 'object') return;
27
+ nodes.push(node);
28
+ for (const child of node.children || []) walk(child);
29
+ }
30
+ walk(tree.source);
31
+ return nodes;
32
+ }
33
+
34
+ function elementRef(node) {
35
+ return { elementId: node.elementId, type: node.type, identifier: node.rawIdentifier, label: node.label };
36
+ }
37
+
38
+ function targetRef(tree, node) {
39
+ return { schemaVersion: schema, runnerEpoch: tree.runtimeBinding.runtimeEpoch,
40
+ bundleId: tree.session.bundleId, processId: tree.session.processId,
41
+ sessionId: tree.session.sessionId, element: elementRef(node) };
42
+ }
43
+
44
+ function sessionRef(tree) {
45
+ return { schemaVersion: sessionSchema, runnerEpoch: tree.runtimeBinding.runtimeEpoch,
46
+ bundleId: tree.session.bundleId, processId: tree.session.processId, sessionId: tree.session.sessionId };
47
+ }
48
+
49
+ function selectNativeNode(tree, selector) {
50
+ if (tree?.nativeTargetSchema !== schema) return reject('ios_native_target_schema_required');
51
+ const nodes = nodesOf(tree).filter(node => node.isVisible === '1'
52
+ && (selector.type === undefined || node.type === selector.type)
53
+ && (selector.accessibilityId === undefined || node.rawIdentifier === selector.accessibilityId)
54
+ && (selector.label === undefined || node.label === selector.label)
55
+ && (selector.elementId === undefined || node.elementId === selector.elementId));
56
+ if (nodes.length !== 1) return { ...reject(nodes.length ? 'ios_native_selector_ambiguous' : 'ios_native_selector_not_found'), matched: nodes.length };
57
+ const node = nodes[0];
58
+ if (typeof node.elementId !== 'string' || !node.elementId) return reject('ios_native_element_identity_required');
59
+ if (node.isEnabled !== '1') return reject('ios_native_target_disabled');
60
+ return { ok: true, node, targetRef: targetRef(tree, node), matched: 1 };
61
+ }
62
+
63
+ function bindNativeTarget(tree, selector, expectedTarget) {
64
+ const selected = selectNativeNode(tree, selector);
65
+ if (!selected.ok) return selected;
66
+ if (expectedTarget !== undefined && !isDeepStrictEqual(selected.targetRef, expectedTarget)) return reject('reobserve_required');
67
+ return selected;
68
+ }
69
+
70
+ module.exports = { schema, selectorSchema, expectedTargetSchema, nodesOf, selectNativeNode, bindNativeTarget, targetRef,
71
+ orientationSchema, expectedSessionSchema, sessionRef };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ function createLiveCaptureQuery({ runner } = {}) {
4
+ if (typeof runner !== 'function') {
5
+ throw new TypeError('runner');
6
+ }
7
+
8
+ async function query(request = {}) {
9
+ const command = commandFor(request);
10
+ if (!command) return unavailablePage('unsupported_capture_stream');
11
+ const { command: _command, stream: _stream, platform: _platform, ...args } = request;
12
+ // CaptureRequest uses null for an absent window boundary. Public command
13
+ // arguments represent absence by omission; no payload or error is replaced.
14
+ for (const key of ['serial', 'packageName', 'afterActionId', 'runtimeEpoch', 'sinceMs', 'timeoutMs']) {
15
+ if (args[key] == null) delete args[key];
16
+ }
17
+ let result;
18
+ try {
19
+ result = await runner(command, {
20
+ ...args,
21
+ limit: request.limit == null ? (command.startsWith('web-') ? 16 : 200) : request.limit,
22
+ });
23
+ } catch (error) {
24
+ return unavailablePage(error.code || error.message || 'capture_query_failed', error);
25
+ }
26
+ if (request.view === 'decision-window' && request.runtimeEpoch != null
27
+ && result?.runtimeEpoch !== request.runtimeEpoch && result?.coverage?.status === 'complete') {
28
+ return unavailablePage('runtime_epoch_changed');
29
+ }
30
+ const expectedTarget = request.targetKey ?? (command.startsWith('ios-') ? request.bundleId : request.packageName);
31
+ if (expectedTarget != null && result?.targetKey !== expectedTarget
32
+ && result?.coverage?.status === 'complete') return unavailablePage('capture_target_mismatch');
33
+ return capturePage(result);
34
+ }
35
+
36
+ return query;
37
+ }
38
+
39
+ // Keep mobile-issued window, epoch, refs and pagination intact through each port.
40
+ // Legacy payloads and explicit failures never acquire invented strong coverage.
41
+ function capturePage(result) {
42
+ const coverage = coverageOf(result);
43
+ if (!result || result.ok === false || result.error || !coverage || coverage.status === 'unavailable') {
44
+ return unavailablePage(result?.error || result?.reason || 'capture_contract_unavailable', result || {});
45
+ }
46
+ if (!Array.isArray(result.refs) || !Array.isArray(result.items)) {
47
+ return unavailablePage('capture_contract_invalid');
48
+ }
49
+ return {
50
+ ...result,
51
+ coverage,
52
+ gap: coverage.gap === true || result.gap === true,
53
+ committed: coverage.committed === true,
54
+ };
55
+ }
56
+
57
+ function coverageOf(result) {
58
+ const coverage = result && result.coverage;
59
+ if (
60
+ !coverage
61
+ || (coverage.status !== 'complete'
62
+ && coverage.status !== 'partial'
63
+ && coverage.status !== 'unavailable')
64
+ ) {
65
+ return null;
66
+ }
67
+ return coverage;
68
+ }
69
+
70
+ // The durable observation and the portable payload verifier share this exact
71
+ // metadata projection. Optional diagnostics remain absent when not supplied.
72
+ function capturePageMetadata(page) {
73
+ return {
74
+ stream: page.stream, coverage: page.coverage, window: page.window,
75
+ runtimeEpoch: page.runtimeEpoch, targetKey: page.targetKey, storeGeneration: page.storeGeneration,
76
+ watermarkCursor: page.watermarkCursor, nextCursor: page.nextCursor,
77
+ hasMore: page.hasMore, throughWatermark: page.throughWatermark,
78
+ ...(page.barrier === undefined ? {} : { barrier: page.barrier }),
79
+ error: page.error || page.reason || null,
80
+ ...(page.field === undefined ? {} : { field: page.field }),
81
+ ...(page.message === undefined ? {} : { message: page.message }),
82
+ };
83
+ }
84
+
85
+ function commandFor(request) {
86
+ const stream = request.stream;
87
+ if (stream !== 'logs' && stream !== 'network' && stream !== 'state' && stream !== 'events') {
88
+ return null;
89
+ }
90
+ if (request.command) {
91
+ const commands = [stream, `ios-${stream}`, `web-${stream}`];
92
+ if (stream === 'logs') commands.push('webview-console');
93
+ if (stream === 'network') commands.push('webview-network');
94
+ return commands.includes(request.command) ? request.command : null;
95
+ }
96
+ if (request.platform === 'ios') return `ios-${stream}`;
97
+ if (request.platform === 'web') return `web-${stream}`;
98
+ return stream;
99
+ }
100
+
101
+ function unavailablePage(error = 'capture_unavailable', { field, message } = {}) {
102
+ return {
103
+ ok: false,
104
+ error,
105
+ ...(field === undefined ? {} : { field }),
106
+ ...(message === undefined ? {} : { message }),
107
+ coverage: { status: 'unavailable', gap: true, committed: false },
108
+ gap: true,
109
+ committed: false,
110
+ refs: [],
111
+ items: [],
112
+ };
113
+ }
114
+
115
+ module.exports = { createLiveCaptureQuery, capturePage, capturePageMetadata, unavailablePage };
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ const { randomUUID, createHash } = require('node:crypto');
4
+ const { currentExecution, checkExecution, withoutExecution } = require('./execution-scope');
5
+
6
+ function terminalReceipt(schema, result, identity) {
7
+ const execution = result?.execution;
8
+ return execution?.schemaVersion === schema && execution.actionId === identity.actionId
9
+ && execution.runtimeEpoch === identity.runtimeEpoch && execution.settled === true
10
+ && result.actionId === identity.actionId && result.runtimeEpoch === identity.runtimeEpoch && result.settled === true
11
+ && typeof result.ok === 'boolean' && typeof result.dispatched === 'boolean' && typeof result.ambiguous === 'boolean'
12
+ && (result.ok === false ? typeof result.error === 'string' && result.error.length > 0 : result.ambiguous === false);
13
+ }
14
+
15
+ // Managed SDK execution validates one original action identity. Closing HTTP
16
+ // is never a completion receipt. Cleanup addresses the exact dispatched peer.
17
+ async function executeManagedAction({ kind, schema, runtime, payload, actionId, timeoutMs, send, cancel }) {
18
+ if (runtime?.executionSchema !== schema || typeof runtime.runtimeEpoch !== 'string' || !runtime.runtimeEpoch) return {
19
+ ok: false, error: `${kind}_execution_unavailable`, dispatched: false, ambiguous: false,
20
+ message: `The app SDK does not advertise managed ${kind} execution. Update the SDK and observe again.`,
21
+ };
22
+ checkExecution();
23
+ const identity = { actionId: actionId ?? randomUUID(), runtimeEpoch: runtime.runtimeEpoch };
24
+ const scope = currentExecution();
25
+ const remaining = Math.min(scope ? scope.deadlineMs - Date.now() : Infinity, timeoutMs, 2147483647);
26
+ if (!(remaining >= 1)) return { ok: false, error: 'deadline_exceeded', dispatched: false, ambiguous: false };
27
+ const request = { ...payload, actionId: identity.actionId,
28
+ execution: { schemaVersion: schema, ...identity, timeoutMs: Math.floor(remaining) } };
29
+ let failure;
30
+ try {
31
+ const result = await send(request);
32
+ if (terminalReceipt(schema, result, identity)) return { ...result, request, executionReceipt: settlementProof(kind, schema, result, identity) };
33
+ if (result?.ok === false && typeof result.error === 'string' && result.error
34
+ && result.dispatched === false && result.ambiguous === false && !result.execution) return { ...result, request, executionReceipt: settlementProof(kind, schema, result, identity) };
35
+ const pending = result?.schemaVersion === schema && result.actionId === identity.actionId
36
+ && result.runtimeEpoch === identity.runtimeEpoch && result.ok === false && result.settled === false
37
+ && result.dispatched === null && result.ambiguous === true && typeof result.error === 'string' && result.error;
38
+ failure = { code: pending ? result.error : `invalid_${kind}_execution_receipt`, response: result };
39
+ } catch (error) {
40
+ if (error.aiAppBridgeRequestNotStarted === true || error.dispatched === false) return {
41
+ ok: false, error: error.code || `${kind}_request_not_started`, message: error.message,
42
+ dispatched: false, ambiguous: false, request,
43
+ };
44
+ failure = error;
45
+ }
46
+ let cancellation;
47
+ try { cancellation = await withoutExecution(() => cancel(identity)); }
48
+ catch (error) { cancellation = { ok: false, error: error.code || error.message }; }
49
+ let receipt;
50
+ if (cancellation?.ok === true && cancellation.actionId === identity.actionId && cancellation.runtimeEpoch === identity.runtimeEpoch
51
+ && terminalReceipt(schema, cancellation.executionResult, identity)) receipt = cancellation.executionResult;
52
+ else if (cancellation?.ok !== false || typeof cancellation.error !== 'string' || !cancellation.error) {
53
+ cancellation = { ok: false, error: `invalid_${kind}_cancel_receipt`, response: cancellation };
54
+ }
55
+ return { ok: false, error: failure.code || `${kind}_action_response_lost`,
56
+ actionId: identity.actionId, runtimeEpoch: identity.runtimeEpoch, request,
57
+ dispatched: receipt?.dispatched ?? null, ambiguous: receipt?.ambiguous ?? true, settled: Boolean(receipt),
58
+ cancellation, executionReceipt: receipt ? settlementProof(kind, schema, receipt, identity) : null, ...(failure.response === undefined ? {} : { response: failure.response }) };
59
+ }
60
+
61
+ function settlementProof(kind, schema, result, identity) {
62
+ const receipt = terminalReceipt(schema, result, identity) ? result
63
+ : result?.cancellation?.ok === true && terminalReceipt(schema, result.cancellation.executionResult, identity)
64
+ ? result.cancellation.executionResult : null;
65
+ if (receipt) {
66
+ const { request, executionReceipt, transport, ...wireReceipt } = receipt;
67
+ return { kind, settled: true, actionId: identity.actionId, runtimeEpoch: identity.runtimeEpoch,
68
+ execution: receipt.execution, dispatched: receipt.dispatched, ambiguous: receipt.ambiguous, error: receipt.error ?? null,
69
+ responseSha256: createHash('sha256').update(JSON.stringify(wireReceipt)).digest('hex') };
70
+ }
71
+ if (result?.dispatched === false && result.ambiguous === false && result.settled !== false && !result.execution) return {
72
+ kind, settled: true, actionId: identity.actionId, runtimeEpoch: identity.runtimeEpoch,
73
+ dispatched: false, ambiguous: false, error: result.error ?? null,
74
+ };
75
+ return null;
76
+ }
77
+
78
+ module.exports = { executeManagedAction, terminalReceipt, settlementProof };
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const managed = require('./managed-sdk-execution');
4
+ const schema = 'aab.native-execution/v1';
5
+
6
+ function executeNativeAction({ runtime, ...options }) {
7
+ return managed.executeManagedAction({ ...options, kind: 'native', schema, runtime: runtime });
8
+ }
9
+
10
+ module.exports = { schema, executeNativeAction,
11
+ terminalReceipt: (result, identity) => managed.terminalReceipt(schema, result, identity),
12
+ settlementProof: (result, identity) => managed.settlementProof('native', schema, result, identity),
13
+ };