@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,239 @@
1
+ 'use strict';
2
+
3
+ const { normalizeExecutionTarget } = require('../shared-kernel/execution-target');
4
+
5
+ const { CommandError, executionFields } = require('../command-errors');
6
+ const { readPermissionState, readPermissionRequest } = require('../android-permissions');
7
+ const { getProcessDeviceMutationLease } = require('../shared-kernel/device-mutation-lease');
8
+ const { checksumOf } = require('../shared-kernel/evidence-schema');
9
+ const { createIntentWorker } = require('./intent-worker');
10
+ const { createProductionIntentDeviceAdapter } = require('./intent-production-adapter');
11
+
12
+ const sameState = (a, b) => a.uid === b.uid && a.userId === b.userId && a.granted === b.granted && JSON.stringify(a.flags) === JSON.stringify(b.flags);
13
+ const sameRequest = (a, b) => a && b && ['token', 'component', 'userId', 'requesterPackage', 'requesterUid'].every(key => a[key] === b[key]);
14
+ const rejection = error => ({ ok: false, error, dispatched: false, ambiguous: false });
15
+
16
+ function permissionDeviceAdapter({ args, origin, expectedState, canDispatch, onDispatch, onFailure, readState, readRequest, ports }) {
17
+ const bridge = ports || require('../device-provider');
18
+ let selectedRequest = null;
19
+ function providerResult(result, stage) {
20
+ if (result.ok || !result.error || /^[a-z][a-z0-9_]+$/.test(result.error)) return result;
21
+ const error = `permission_ui_${stage}_failed`;
22
+ onFailure({ stage, error, message: String(result.error).slice(0, 2048) });
23
+ return { ...result, error };
24
+ }
25
+ async function input(action) {
26
+ let state; let request;
27
+ try {
28
+ state = await readState(args);
29
+ if (!sameState(expectedState(), state)) return rejection('permission_state_changed');
30
+ request = await readRequest(args);
31
+ if (!sameRequest(origin(), request.request) || !sameRequest(selectedRequest, request.request)) return rejection('permission_request_changed');
32
+ } catch (error) { return rejection(error.code || 'permission_query_failed'); }
33
+ if (!canDispatch()) return rejection('cancelled');
34
+ onDispatch();
35
+ try {
36
+ return { ...await action(), precondition: { request, state } };
37
+ } catch (error) {
38
+ return { ok: false, error: 'permission_input_failed', dispatched: error.code === 'ENOENT' ? false : true,
39
+ ambiguous: error.code !== 'ENOENT', cause: error.code || null, precondition: { request, state }, ...executionFields(error) };
40
+ }
41
+ }
42
+ const base = createProductionIntentDeviceAdapter({ adb: args.adb, ports: {
43
+ ...bridge,
44
+ uiaTap: (ctx, binding, options) => input(() => bridge.uiaTap(ctx, binding, options)),
45
+ keyevent: ctx => input(() => bridge.keyevent(ctx, 4)),
46
+ } });
47
+ return {
48
+ async observe(request) {
49
+ try {
50
+ const before = await readRequest(args);
51
+ if (!sameRequest(origin(), before.request)) return rejection('permission_request_changed');
52
+ const observed = await base.observe({ ...request, provider: 'uia', packageName: before.request.packageName, foregroundPackages: [] });
53
+ if (!observed.ok) return providerResult(observed, 'observation');
54
+ const after = await readRequest(args);
55
+ if (!sameRequest(before.request, after.request) || after.request.component !== `${observed.route.packageName}/${observed.route.activity}`) return rejection('permission_request_changed');
56
+ return { ...observed, route: { ...observed.route, permissionRequest: after.request } };
57
+ } catch (error) { return rejection(error.code || 'permission_request_query_failed'); }
58
+ },
59
+ async action(request) {
60
+ selectedRequest = request.route?.permissionRequest;
61
+ if (!sameRequest(origin(), selectedRequest)) return rejection('permission_request_changed');
62
+ return providerResult(await base.action({ ...request, primaryProvider: 'uia', foregroundPackages: [origin().packageName],
63
+ spec: { ...request.spec, provider: 'uia', exact: true, requireClickable: true } }), 'action');
64
+ },
65
+ };
66
+ }
67
+
68
+ async function createPermissionIntent({ args, operationId, store, recording, dependencies = {} }) {
69
+ const ownership = (dependencies.lease || getProcessDeviceMutationLease()).acquire(args.serial);
70
+ if (!ownership.ok) throw new CommandError(ownership.error, 'The device is owned by another operation.', { details: { serial: args.serial } });
71
+ const readState = dependencies.readState || readPermissionState;
72
+ const readRequest = dependencies.readRequest || readPermissionRequest;
73
+ const bound = { ...args };
74
+ const target = normalizeExecutionTarget({ platform: 'android', serial: args.serial, packageName: args.packageName, foregroundPackages: [],
75
+ ...(args.adb === undefined ? {} : { adb: args.adb }) }, { intent: true });
76
+ let worker; let active = null; let finalizing = null; let final = false; let stopping = null; let timer; let deadline = Infinity;
77
+ let expected; let origin; let dispatched = false; let ownedReceipt = null;
78
+ const permissionDialog = { kind: 'permission-dialog', permission: args.permission, outcome: args.outcome,
79
+ phase: 'preparing', before: null, after: null, requestBefore: null, requestAfter: null, verified: false, actionCount: 0 };
80
+ try {
81
+ const adapter = permissionDeviceAdapter({ args: bound, origin: () => origin, expectedState: () => expected,
82
+ canDispatch: () => Date.now() < deadline && !stopping && !finalizing && !final && worker.context.canDispatch(),
83
+ onDispatch: () => { dispatched = true; permissionDialog.actionCount++; }, readState, readRequest,
84
+ onFailure: failure => { permissionDialog.providerFailure = failure; },
85
+ ports: dependencies.ports });
86
+ worker = createIntentWorker({ operationId, target, ownsOutcome: false, timeoutMs: null, provider: 'uia', store, recording, adapter,
87
+ goal: `Resolve the observed runtime permission request from ${args.packageName}: ${args.outcome} ${args.permission}. Read the actual UI and select exactly one visible control. Completion requires PackageManager state and closure of this Activity instance. No recording or other App action is part of this workflow.` });
88
+ } catch (error) { ownership.release(); throw error; }
89
+ function snapshot() {
90
+ return { ...worker.status(), permissionDialog: { ...permissionDialog } };
91
+ }
92
+ async function persist(kind, record) {
93
+ const result = await store.persist(kind, { operationId, target, revision: worker.runtime.state.revision, timestampMs: Date.now(), ...record });
94
+ if (!result.ok) throw new CommandError(result.error || 'permission_evidence_failed', 'Permission evidence could not be committed.');
95
+ return result;
96
+ }
97
+ async function inspect() {
98
+ permissionDialog.after = await readState(bound);
99
+ permissionDialog.requestAfter = await readRequest(bound);
100
+ permissionDialog.dialogClosed = !permissionDialog.requestAfter.activeRequestTokens.includes(origin.token);
101
+ permissionDialog.pending = !permissionDialog.dialogClosed && !permissionDialog.requestAfter.request ? 'activity_closure' : null;
102
+ }
103
+ async function verify() {
104
+ await inspect();
105
+ if (permissionDialog.after.uid !== permissionDialog.before.uid) return { status: 'intervention_required', error: 'permission_target_changed' };
106
+ const request = permissionDialog.requestAfter.request;
107
+ if (request && !sameRequest(origin, request)) return { status: 'intervention_required', error: 'permission_request_changed' };
108
+ if (!ownedReceipt) {
109
+ if (permissionDialog.dialogClosed || !sameState(expected, permissionDialog.after)) return { status: 'intervention_required', error: 'permission_changed_without_owned_action' };
110
+ return null;
111
+ }
112
+ expected = permissionDialog.after;
113
+ if (!permissionDialog.dialogClosed) {
114
+ if (permissionDialog.pending) {
115
+ worker.runtime.state.status = 'waiting_for_observation'; worker.runtime.state.error = null;
116
+ }
117
+ return null;
118
+ }
119
+ const after = permissionDialog.after;
120
+ const matches = args.outcome === 'allow' ? after.granted && !after.flags.includes('ONE_TIME')
121
+ : args.outcome === 'allow-once' ? after.granted && after.flags.includes('ONE_TIME')
122
+ : args.outcome === 'deny' ? !after.granted && after.flags.includes('USER_SET')
123
+ : sameState(permissionDialog.before, after);
124
+ permissionDialog.verified = matches;
125
+ return matches ? { status: 'completed', error: null } : { status: 'failed', error: 'permission_outcome_mismatch' };
126
+ }
127
+ function finish(verdict) {
128
+ if (finalizing) return finalizing;
129
+ clearTimeout(timer);
130
+ worker.runtime.state.status = 'finishing';
131
+ finalizing = (async () => {
132
+ try {
133
+ await worker.quiesce();
134
+ worker.runtime.state.status = 'finishing';
135
+ if (verdict.status !== 'completed') {
136
+ permissionDialog.verified = false;
137
+ // Cancel stops this operation, never presses Back and never rolls back
138
+ // an Android grant. Report the final observed state after in-flight I/O.
139
+ try { if (origin) await inspect(); }
140
+ catch (error) { permissionDialog.verificationError = error.code || 'permission_query_failed'; }
141
+ }
142
+ permissionDialog.phase = verdict.status;
143
+ const proof = await persist('checkpoint', { stepId: 'permission-outcome', payloadSummary: { ...verdict, permissionDialog: { ...permissionDialog } } });
144
+ permissionDialog.verificationEvidenceId = proof.evidenceId;
145
+ worker.runtime.state.status = verdict.status; worker.runtime.state.error = verdict.error;
146
+ worker.runtime.emit('permission_verified', { status: verdict.status, verified: permissionDialog.verified, evidenceId: proof.evidenceId });
147
+ } catch (error) {
148
+ permissionDialog.phase = 'blocked_evidence_store'; permissionDialog.verified = false;
149
+ worker.runtime.state.status = 'blocked_evidence_store'; worker.runtime.state.error = error.code || 'permission_evidence_failed';
150
+ } finally { final = true; ownership.release(); }
151
+ return snapshot();
152
+ })();
153
+ return finalizing;
154
+ }
155
+ async function stop(status, error) {
156
+ if (final || finalizing) { await finalizing; return snapshot(); }
157
+ stopping ||= { status, error };
158
+ const uiStopped = worker.cancel();
159
+ await uiStopped;
160
+ await active;
161
+ return finish(stopping);
162
+ }
163
+ async function run(fn) {
164
+ if (final || finalizing || stopping) return { ...snapshot(), ok: false, error: 'operation_stopped' };
165
+ if (active) return { ...snapshot(), ok: false, error: 'operation_busy' };
166
+ let result; let verdict;
167
+ active = ownership.run(async () => {
168
+ try { ({ result, verdict } = await fn()); }
169
+ catch (error) { verdict = { status: dispatched ? 'inconclusive' : 'failed', error: error.code || 'permission_verification_failed' }; }
170
+ });
171
+ await active;
172
+ active = null;
173
+ if (Date.now() >= deadline) stopping ||= { status: 'timeout', error: 'permission_dialog_timeout' };
174
+ if (stopping) return finish(stopping);
175
+ if (verdict) return finish(verdict);
176
+ permissionDialog.phase = worker.runtime.state.status;
177
+ return { ...snapshot(), ...(result ? { ok: result.ok, error: result.error } : {}) };
178
+ }
179
+ function workerFailure(result) {
180
+ return ['failed', 'ambiguous', 'blocked_evidence_store'].includes(result.status) ? { status: result.status, error: result.error } : null;
181
+ }
182
+ return {
183
+ ...worker, isManagedWorkflow: true, isFinished: () => final, status: snapshot,
184
+ start: () => run(async () => {
185
+ deadline = Date.now() + (args.timeoutMs || 60000);
186
+ timer = setTimeout(() => { stop('timeout', 'permission_dialog_timeout'); }, args.timeoutMs || 60000); timer.unref?.();
187
+ permissionDialog.before = expected = await readState(bound);
188
+ bound.userId = expected.userId;
189
+ permissionDialog.requestBefore = await readRequest(bound);
190
+ origin = permissionDialog.requestBefore.request;
191
+ if (!origin) throw new CommandError('permission_dialog_not_found', 'Trigger a runtime permission request through the App before starting permission-dialog.');
192
+ if (origin.requesterPackage !== args.packageName || origin.requesterUid !== expected.uid || origin.userId !== expected.userId) {
193
+ throw new CommandError('permission_requester_mismatch', 'The visible runtime permission request belongs to a different App or Android user.');
194
+ }
195
+ const plan = await persist('plan', { planStepId: 'permission-request', actionSpecHash: checksumOf({ target, outcome: args.outcome, permission: args.permission }),
196
+ payloadSummary: { permissionDialog: { ...permissionDialog } } });
197
+ permissionDialog.planEvidenceId = plan.evidenceId;
198
+ if (stopping) return {};
199
+ const result = await worker.start();
200
+ return { result, verdict: result.ok ? null : { status: 'inconclusive', error: result.error } };
201
+ }),
202
+ observe: options => run(async () => {
203
+ const verdict = await verify();
204
+ if (verdict || stopping) return { verdict };
205
+ if (permissionDialog.pending) return { result: { ok: true, error: null } };
206
+ const result = await worker.observe(options);
207
+ return { result, verdict: workerFailure(result) };
208
+ }),
209
+ decide: decision => run(async () => {
210
+ const action = decision?.action;
211
+ const allowed = action?.action === 'tap' && action.selector && Object.keys(action).every(key => ['action', 'selector', 'provider', 'exact', 'requireClickable'].includes(key))
212
+ || args.outcome === 'dismiss' && action?.action === 'back' && Object.keys(action).every(key => ['action', 'provider'].includes(key));
213
+ if (decision?.agentDecision !== 'act' || !allowed || action?.exact === false || action?.provider && action.provider !== 'uia') {
214
+ return { result: { ok: false, error: 'permission_requires_observed_decision', message: 'Use an exact observed tap; dismiss also accepts back. Completion is verified independently. Use intent cancel to stop the operation.' } };
215
+ }
216
+ const previous = worker.context.latestEvidenceIds.receipt;
217
+ const result = await worker.decide(decision);
218
+ const failure = workerFailure(result);
219
+ if (failure || stopping) return { result, verdict: failure };
220
+ const receiptId = worker.context.latestEvidenceIds.receipt;
221
+ if (receiptId && receiptId !== previous) {
222
+ const receipt = store.read(receiptId);
223
+ if (!receipt.ok || receipt.record.mechanicalStatus !== 'ok' || receipt.record.ambiguous || !receipt.record.dispatched) {
224
+ return { verdict: { status: 'inconclusive', error: 'permission_action_not_verified' } };
225
+ }
226
+ permissionDialog.actionEvidenceId = ownedReceipt = receiptId;
227
+ const verdict = await verify();
228
+ return { result: permissionDialog.pending ? { ok: true, error: null } : result, verdict };
229
+ }
230
+ return { result };
231
+ }),
232
+ cancel: () => stop('cancelled', 'permission_dialog_cancelled'),
233
+ intervene: reason => stop('intervention_required', reason || 'permission_dialog_intervened'),
234
+ pause() { if (!active && !finalizing && !stopping && !final) worker.pause(); return snapshot(); },
235
+ resume() { if (!active && !finalizing && !stopping && !final) worker.resume(); return snapshot(); },
236
+ };
237
+ }
238
+
239
+ module.exports = { createPermissionIntent, permissionDeviceAdapter, sameState, sameRequest };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const { selectWebNode, schema } = require('../shared-kernel/web-dom-target');
4
+
5
+ function createWebIntentDeviceAdapter({ provider } = {}) {
6
+ if (!provider || typeof provider.run !== 'function') throw new TypeError('Web Intent requires the serving Web provider.');
7
+ const bound = target => ({ sessionId: target.sessionId, runtimeEpoch: target.runtimeEpoch, targetId: target.targetId ?? 'main' });
8
+ return {
9
+ async observe(target) {
10
+ const rawTree = await provider.run('web-dom', bound(target));
11
+ if (rawTree.ok !== true) return rawTree;
12
+ if (rawTree.webTargetSchema !== schema) return { ok: false, error: 'web_dom_target_schema_required' };
13
+ return { ok: true, provider: 'h5', rawTreeId: target.rawTreeId, rawTree, foregroundTarget: rawTree.pageRef.url };
14
+ },
15
+ async action(target) {
16
+ const { spec, rawTree } = target;
17
+ const selected = selectWebNode(rawTree, spec.selector);
18
+ if (!selected.ok) return selected;
19
+ const commands = { tap: 'web-click', inputText: 'web-input', pressKey: 'web-key', scroll: 'web-scroll', scrollBy: 'web-scroll' };
20
+ const result = await provider.run(commands[spec.action], { ...bound(target), runtimeActionId: target.actionId,
21
+ selector: { elementId: selected.node.elementId }, expectedTarget: selected.targetRef,
22
+ ...(spec.action === 'inputText' ? { value: spec.value } : {}),
23
+ ...(spec.action === 'pressKey' ? { key: spec.key } : {}),
24
+ ...(spec.action === 'scroll' ? { mode: 'into-view' } : {}),
25
+ ...(spec.action === 'scrollBy' ? { mode: 'by', deltaX: spec.deltaX, deltaY: spec.deltaY } : {}) });
26
+ return { ...result, mechanicalStatus: result.ok === true ? 'ok' : 'failed' };
27
+ },
28
+ };
29
+ }
30
+
31
+ module.exports = { createWebIntentDeviceAdapter };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+ const { isDeepStrictEqual } = require('node:util');
3
+
4
+ // A normal process exit alone cannot settle a remote action. Only the original
5
+ // devicectl JSON response, with its exact arguments and explicit OS rejection,
6
+ // can identify a known device rejection. Timeout/transport loss stays unknown.
7
+ function deviceCommandRejection(reply, args, error) {
8
+ if (!Number.isInteger(error?.code) || error.code === 0 || error.signal || error.killed) return null;
9
+ return originalDeviceRejection(reply, args);
10
+ }
11
+
12
+ // For recovery, the caller must independently associate the retained original
13
+ // response and exact invocation with the pending operation before using it.
14
+ function originalDeviceRejection(reply, args) {
15
+ const command = args.slice(0, 3).join(' ');
16
+ const type = command === 'device process launch' ? 'devicectl.device.process.launch'
17
+ : command === 'device install app' ? 'devicectl.device.install.app' : null;
18
+ if (!type
19
+ || reply?.info?.jsonVersion !== 4 || reply.info.outcome !== 'failed'
20
+ || reply.info.commandType !== type
21
+ || !isDeepStrictEqual(reply.info.arguments, ['devicectl', ...args])) return null;
22
+ const errors = [];
23
+ function visit(value, depth) {
24
+ if (!value || typeof value !== 'object' || depth > 16 || errors.length > 32) return;
25
+ if (typeof value.domain === 'string' && Number.isInteger(value.code)) errors.push(value);
26
+ for (const item of Object.values(value)) visit(item, depth + 1);
27
+ }
28
+ visit(reply.error, 0);
29
+ if (command === 'device install app') {
30
+ const installer = errors.find(value => value.domain === 'MIInstallerErrorDomain' && value.code === 13);
31
+ if (!errors.some(value => value.domain === 'com.apple.dt.CoreDeviceError' && value.code === 3002)
32
+ || !errors.some(value => value.domain === 'IXUserPresentableErrorDomain' && value.code === 14)
33
+ || installer?.userInfo?.LegacyErrorString?.string !== 'ApplicationVerificationFailed'
34
+ || !installer.userInfo.NSLocalizedDescription?.string?.startsWith('This device has reached the maximum number of installed apps using a free developer profile:')) return null;
35
+ return { ok: false, error: 'ios_free_profile_app_limit',
36
+ message: 'The selected iPhone has reached the free developer profile App limit. Remove an explicitly selected test App before installing another.',
37
+ settled: true, dispatched: true, ambiguous: false, deviceOutcome: reply };
38
+ }
39
+ const reason = errors.find(value => value.domain === 'FBSOpenApplicationErrorDomain' && [3, 7].includes(value.code));
40
+ if (!errors.some(value => value.domain === 'FBSOpenApplicationServiceErrorDomain' && value.code === 1) || !reason) return null;
41
+ return { ok: false, error: reason.code === 7 ? 'ios_device_locked' : 'ios_app_launch_rejected',
42
+ message: reason.code === 7 ? 'Unlock the selected iPhone before launching the App.'
43
+ : 'iOS rejected the App launch. Check its code signature, entitlements and developer trust on the selected device.',
44
+ settled: true, dispatched: true, ambiguous: false, deviceOutcome: reply };
45
+ }
46
+
47
+ module.exports = { deviceCommandRejection, originalDeviceRejection };
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ const { randomUUID } = require('node:crypto');
4
+ const { CommandError } = require('./command-errors');
5
+ const managed = require('./shared-kernel/managed-sdk-execution');
6
+ const { runDeviceEffect } = require('./shared-kernel/device-mutation-lease');
7
+ const { checkExecution, runExecution } = require('./shared-kernel/execution-scope');
8
+
9
+ const schemas = { h5: 'aab.h5-execution/v1', flutter: 'aab.flutter-execution/v1', wda: 'aab.wda-execution/v1' };
10
+
11
+ function iosDeviceKey(device) {
12
+ if (typeof device?.udid !== 'string' || !device.udid) {
13
+ throw new CommandError('ios_device_identity_unavailable', 'devicectl must supply the physical iOS UDID before mutation or recovery.');
14
+ }
15
+ return `ios:${device.udid}`;
16
+ }
17
+
18
+ async function lookupCompletion(port, kind, identity, first) {
19
+ let page = first;
20
+ let after = 0;
21
+ let through;
22
+ let cursor;
23
+ while (true) {
24
+ checkExecution();
25
+ if (!page) {
26
+ const query = new URLSearchParams({ ...(kind === 'wda' ? {} : { kind }), ...identity, ...(cursor === undefined ? {} : { cursor }) });
27
+ page = await port.get(`${kind === 'wda' ? '/aab/execution/result' : '/v1/execution/result'}?${query}`);
28
+ }
29
+ if (page.ok !== true) return page;
30
+ if (page.executionResult) {
31
+ if (page.actionId !== identity.actionId || page.runtimeEpoch !== identity.runtimeEpoch
32
+ || !managed.terminalReceipt(schemas[kind], page.executionResult, identity)) {
33
+ return { ok: false, error: 'invalid_ios_completion_receipt', settled: false };
34
+ }
35
+ return page;
36
+ }
37
+ if (page.found !== false || typeof page.hasMore !== 'boolean') return { ok: false, error: 'invalid_ios_completion_page', settled: false };
38
+ if (!page.hasMore) return { ok: false, error: 'ios_original_completion_unavailable', settled: false, ...identity };
39
+ if (!Number.isSafeInteger(page.nextSequence) || page.nextSequence <= after
40
+ || !Number.isSafeInteger(page.throughSequence) || page.nextSequence >= page.throughSequence
41
+ || typeof page.nextCursor !== 'string' || !page.nextCursor || page.nextCursor.length > 2048
42
+ || (through !== undefined && page.throughSequence !== through)) {
43
+ return { ok: false, error: 'invalid_ios_completion_cursor', settled: false };
44
+ }
45
+ after = page.nextSequence; through = page.throughSequence; cursor = page.nextCursor; page = null;
46
+ }
47
+ }
48
+
49
+ async function executeIOSAction({ port, kind, payload, status, target, timeoutMs, actionId }) {
50
+ if (kind === 'h5' && status.debugBridge?.h5TargetSchema !== require('./shared-kernel/ios-h5-target').schema) {
51
+ return { ok: false, error: 'ios_h5_target_schema_required', dispatched: false, ambiguous: false,
52
+ message: 'Update the selected App SDK to one that binds H5 actions to observed WebViews and documents.' };
53
+ }
54
+ const runtime = kind === 'h5'
55
+ ? { runtimeEpoch: status.debugBridge?.runtimeEpoch, executionSchema: status.debugBridge?.h5ExecutionSchema }
56
+ : { runtimeEpoch: status.flutter?.layout?.operable?.runtimeEpoch, executionSchema: status.debugBridge?.flutterExecutionSchema };
57
+ if (runtime.executionSchema !== schemas[kind] || typeof runtime.runtimeEpoch !== 'string' || !runtime.runtimeEpoch) {
58
+ return { ok: false, error: 'ios_managed_execution_unavailable', dispatched: false, ambiguous: false,
59
+ message: 'The selected iOS SDK/Flutter engine must advertise managed execution before an action can run.' };
60
+ }
61
+ const identity = { actionId: actionId ?? randomUUID(), runtimeEpoch: runtime.runtimeEpoch };
62
+ return runDeviceEffect({ kind: `ios-${kind}`, ...identity, target }, async () => {
63
+ const result = await managed.executeManagedAction({ kind, schema: schemas[kind], runtime, payload,
64
+ actionId: identity.actionId, timeoutMs,
65
+ send: request => port.post(kind === 'h5' ? '/v1/h5/action' : '/v1/flutter/action', request),
66
+ cancel: original => runExecution({ timeoutMs: 4000 }, async () => {
67
+ const cancelled = await port.post(`/v1/${kind}/cancel`, original);
68
+ if (cancelled.ok !== true) return cancelled;
69
+ return lookupCompletion(port, kind, original, cancelled);
70
+ }),
71
+ });
72
+ return { ...result, executionReceipt: managed.settlementProof(`ios-${kind}`, schemas[kind], result, identity) };
73
+ }, result => result.executionReceipt);
74
+ }
75
+
76
+ async function reconcileIOS({ lease, device, args, createPort }) {
77
+ return lease.reconcile(iosDeviceKey(device), async pending => {
78
+ const kind = pending.kind === 'ios-h5' ? 'h5' : pending.kind === 'ios-flutter' ? 'flutter' : null;
79
+ if (!kind || pending.target?.deviceId !== device.udid || pending.target?.bundleId !== args.bundleId
80
+ || typeof pending.actionId !== 'string' || typeof pending.runtimeEpoch !== 'string') {
81
+ return { settled: false, error: 'ios_original_completion_identity_required' };
82
+ }
83
+ const identity = { actionId: pending.actionId, runtimeEpoch: pending.runtimeEpoch };
84
+ try {
85
+ // Reconnect to the original device/App. The process serving durable
86
+ // history may be new; the queried action/epoch can never be replaced.
87
+ const target = { ...pending.target, timeoutMs: args.timeoutMs ?? 30000 };
88
+ if (args.runtimeUrl !== undefined) {
89
+ delete target.iosHost; delete target.iosPort; target.runtimeUrl = args.runtimeUrl;
90
+ } else if (args.iosHost !== undefined || args.iosPort !== undefined) {
91
+ delete target.runtimeUrl;
92
+ for (const key of ['iosHost', 'iosPort']) if (args[key] !== undefined) target[key] = args[key];
93
+ }
94
+ if (args.devicectl !== undefined) target.devicectl = args.devicectl;
95
+ const port = await createPort(target);
96
+ const reply = await lookupCompletion(port, kind, identity);
97
+ if (reply.ok !== true || reply.receipt?.committed !== true
98
+ || !Number.isSafeInteger(reply.receipt?.sequence) || reply.receipt.sequence < 1
99
+ || !/^[a-f0-9]{64}$/.test(reply.receipt?.sha256)) {
100
+ return { settled: false, error: reply.error || 'ios_durable_completion_required' };
101
+ }
102
+ return managed.settlementProof(`ios-${kind}`, schemas[kind], reply.executionResult, identity)
103
+ || { settled: false, error: 'invalid_ios_completion_receipt' };
104
+ } catch (error) { return { settled: false, error: error.code || 'ios_completion_query_failed', message: error.message }; }
105
+ });
106
+ }
107
+
108
+ module.exports = { iosDeviceKey, executeIOSAction, lookupCompletion, reconcileIOS };