@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,267 @@
1
+ 'use strict';
2
+
3
+ const { normalizeExecutionTarget } = require('../shared-kernel/execution-target');
4
+
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const crypto = require('node:crypto');
9
+ const { execFile } = require('node:child_process');
10
+ const { promisify } = require('node:util');
11
+ const execute = promisify(execFile);
12
+ const { CommandError } = require('../command-errors');
13
+ const { getProcessDeviceMutationLease } = require('../shared-kernel/device-mutation-lease');
14
+ const { checksumOf } = require('../shared-kernel/evidence-schema');
15
+ const { createProductionIntentDeviceAdapter } = require('./intent-production-adapter');
16
+ const { createIntentWorker } = require('./intent-worker');
17
+ const { prepareInstallJob, settlementProof } = require('../shared-kernel/android-install-execution');
18
+
19
+ async function sha256(file) {
20
+ const hash = crypto.createHash('sha256');
21
+ for await (const chunk of fs.createReadStream(file)) hash.update(chunk);
22
+ return hash.digest('hex');
23
+ }
24
+
25
+ function androidBuildTool(name, explicit) {
26
+ if (explicit) return explicit;
27
+ const sdk = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME
28
+ || path.join(os.homedir(), process.platform === 'darwin' ? 'Library/Android/sdk' : 'Android/Sdk');
29
+ const root = path.join(sdk, 'build-tools');
30
+ const suffix = process.platform === 'win32' ? (name === 'apksigner' ? '.bat' : '.exe') : '';
31
+ const versions = fs.existsSync(root) ? fs.readdirSync(root).sort((a, b) => b.localeCompare(a, undefined, { numeric: true })) : [];
32
+ const found = versions.map(version => path.join(root, version, name + suffix)).find(file => fs.existsSync(file));
33
+ if (!found) throw new CommandError('apk_inspector_unavailable', `Android SDK ${name} is required. Supply ${name}Path or configure ANDROID_SDK_ROOT.`, { field: `${name}Path` });
34
+ return found;
35
+ }
36
+
37
+ async function inspectApk(args, run = execute) {
38
+ const file = path.resolve(args.apkPath);
39
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) throw new CommandError('invalid_apk', 'apkPath must be an existing APK file.', { field: 'apkPath' });
40
+ let badging; let signatures;
41
+ try {
42
+ [badging, signatures] = await Promise.all([
43
+ run(androidBuildTool('aapt', args.aaptPath), ['dump', 'badging', file], { timeout: 30000, maxBuffer: 4 * 1024 * 1024 }),
44
+ run(androidBuildTool('apksigner', args.apksignerPath), ['verify', '--print-certs', file], { timeout: 30000, maxBuffer: 1024 * 1024 }),
45
+ ]);
46
+ } catch (error) {
47
+ if (error instanceof CommandError) throw error;
48
+ throw new CommandError('invalid_apk', 'APK manifest or signature verification failed.', { field: 'apkPath', details: { cause: error.code || null } });
49
+ }
50
+ const pkg = /^package: name='([^']+)' versionCode='([^']+)' versionName='([^']*)'/m.exec(badging.stdout);
51
+ const certificates = [...new Set([...signatures.stdout.matchAll(/^(?:Signer #\d+|V(?:[124]|3(?:\.[01])?) Signer:) certificate SHA-256 digest: ([a-fA-F0-9]{64})\r?$/gm)].map(match => match[1].toLowerCase()))];
52
+ if (!pkg || !certificates.length) throw new CommandError('invalid_apk', 'The APK has no verified package identity.', { field: 'apkPath' });
53
+ if (args.packageName !== undefined && args.packageName !== pkg[1]) throw new CommandError('apk_package_mismatch', 'packageName differs from the APK manifest.', { field: 'packageName', details: { requested: args.packageName, apk: pkg[1] } });
54
+ return { path: file, sha256: await sha256(file), packageName: pkg[1], versionCode: pkg[2], versionName: pkg[3], certificates, bytes: fs.statSync(file).size };
55
+ }
56
+
57
+ async function prepareApk(args) {
58
+ const sourcePath = path.resolve(args.apkPath);
59
+ if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) throw new CommandError('invalid_apk', 'apkPath must be an existing APK file.', { field: 'apkPath' });
60
+ const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'aab-install-'));
61
+ const release = () => fs.rmSync(directory, { recursive: true, force: true });
62
+ try {
63
+ const stagedPath = path.join(directory, 'verified.apk');
64
+ await fs.promises.copyFile(sourcePath, stagedPath, fs.constants.COPYFILE_EXCL);
65
+ fs.chmodSync(stagedPath, 0o400);
66
+ const artifact = await inspectApk({ ...args, apkPath: stagedPath });
67
+ return { artifact: { ...artifact, sourcePath }, release };
68
+ } catch (error) { release(); throw error; }
69
+ }
70
+
71
+
72
+ async function installedIdentity(args, artifact, run = execute) {
73
+ const adb = args.adb || process.env.ADB || 'adb';
74
+ const command = values => run(adb, ['-s', args.serial, ...values], { timeout: args.adbTimeoutMs || 15000, maxBuffer: 1024 * 1024 });
75
+ let output;
76
+ try { output = await command(['shell', 'pm', 'path', artifact.packageName]); }
77
+ catch (error) {
78
+ // Empty exit-1 is the package manager's absent-package response. Connectivity errors remain unknown.
79
+ if (error.code === 1 && !String(error.stdout || '').trim() && !String(error.stderr || '').trim()) return { known: true, installed: false, identityMatches: false };
80
+ return { known: false, installed: null, identityMatches: false, error: error.code || 'package_query_failed' };
81
+ }
82
+ const paths = output.stdout.trim().split(/\r?\n/).filter(Boolean);
83
+ if (!paths.length) return { known: true, installed: false, identityMatches: false };
84
+ if (paths.length !== 1 || !paths[0].startsWith('package:/data/app/') || !paths[0].endsWith('/base.apk')) {
85
+ return { known: true, installed: true, identityMatches: false, error: 'installed_apk_layout_unsupported' };
86
+ }
87
+ const devicePath = paths[0].slice('package:'.length);
88
+ try {
89
+ const digest = (await command(['shell', 'sha256sum', devicePath])).stdout.trim().split(/\s+/)[0];
90
+ if (!/^[a-f0-9]{64}$/.test(digest)) return { known: false, installed: true, identityMatches: false, error: 'installed_hash_unavailable' };
91
+ return { known: true, installed: true, path: devicePath, sha256: digest, identityMatches: digest === artifact.sha256,
92
+ identitySource: 'exact installed APK bytes compared with the locally verified manifest and signer' };
93
+ } catch (error) { return { known: false, installed: true, identityMatches: false, error: error.code || 'installed_hash_unavailable' }; }
94
+ }
95
+
96
+ // This adapter only discovers the current system window. The Agent chooses an
97
+ // exact selector through the ordinary Intent decision contract. No UI label or
98
+ // coordinate is stored here, and a failed action never switches provider.
99
+ function installerDeviceAdapter(args, ports, deviceAdapter) {
100
+ const bridge = ports || require('../device-provider');
101
+ const base = deviceAdapter || createProductionIntentDeviceAdapter({ ports: bridge, adb: args.adb });
102
+ return {
103
+ async observe(request) {
104
+ const ctx = bridge.createBridgeContext({ ...args, packageName: request.packageName });
105
+ const foreground = await bridge.foregroundWindow(ctx);
106
+ if (!foreground.ok || !foreground.packageName) return { ok: false, error: 'foreground_probe_failed' };
107
+ return base.observe({ ...request, packageName: foreground.packageName, provider: 'uia', foregroundPackages: [] });
108
+ },
109
+ async action(request) {
110
+ if (!request.route || request.spec?.action !== 'tap' || !request.spec?.selector) {
111
+ return { ok: false, error: 'installer_requires_observed_exact_selector', dispatched: false, ambiguous: false };
112
+ }
113
+ const ctx = bridge.createBridgeContext({ ...args, packageName: request.route.packageName });
114
+ const foreground = await bridge.foregroundWindow(ctx);
115
+ if (!foreground.ok || foreground.component !== request.route.component) {
116
+ return { ok: false, error: 'reobserve_required', dispatched: false, ambiguous: false };
117
+ }
118
+ return base.action({ ...request, primaryProvider: 'uia', foregroundPackages: [request.route.packageName],
119
+ spec: { ...request.spec, provider: 'uia', exact: true, requireClickable: true } });
120
+ },
121
+ };
122
+ }
123
+
124
+ async function createInstallIntent({ args, operationId, store, recording, dependencies = {} }) {
125
+ const prepared = await (dependencies.prepareApk || prepareApk)(args);
126
+ const { artifact } = prepared;
127
+ const target = normalizeExecutionTarget({ platform: 'android', serial: args.serial, packageName: artifact.packageName, foregroundPackages: [],
128
+ ...(args.adb === undefined ? {} : { adb: args.adb }) }, { intent: true });
129
+ let ownership;
130
+ try { ownership = (dependencies.lease || getProcessDeviceMutationLease()).acquire(args.serial); }
131
+ catch (error) { prepared.release(); throw error; }
132
+ if (!ownership.ok) { prepared.release(); throw new CommandError(ownership.error, 'The device is owned by another operation.', { details: { serial: args.serial } }); }
133
+ const release = () => { try { ownership.release(); } finally { prepared.release(); } };
134
+ let worker; let installOwnership;
135
+ try {
136
+ const adapter = dependencies.adapter || installerDeviceAdapter(args);
137
+ worker = createIntentWorker({ operationId, target, ownsOutcome: false, timeoutMs: null, provider: 'uia', store, adapter, recording,
138
+ goal: `Install the verified APK ${artifact.packageName} ${artifact.versionName}. Inspect actual system pages and choose exact selectors. Completion requires the independent installed APK identity check.` });
139
+ } catch (error) { release(); throw error; }
140
+ let job; let finished; let starting; let finalizing = false; let final = false; let activeUi = null; let cancelled = false;
141
+ let uiFailure = null;
142
+ let installation = { kind: 'install-apk', artifact, before: null, after: null, phase: 'preparing', requestSucceeded: false, verified: false };
143
+ const snapshot = args => {
144
+ const status = worker.status(args);
145
+ if (['waiting_for_observation', 'finishing', 'paused'].includes(status.status) && !status.error) status.ok = true;
146
+ return { ...status, installation: { ...installation, ...(job && !finalizing && !final ? { process: job.status() } : {}) } };
147
+ };
148
+ async function persist(kind, record) {
149
+ const receipt = await store.persist(kind, { operationId, revision: worker.runtime.state.revision, target, timestampMs: Date.now(), ...record });
150
+ if (!receipt.ok) throw new CommandError(receipt.error || 'installation_evidence_failed', 'Installation evidence could not be committed.');
151
+ return receipt;
152
+ }
153
+ async function finalize(result) {
154
+ finalizing = true;
155
+ await worker.quiesce();
156
+ worker.runtime.state.status = 'finishing';
157
+ await activeUi?.catch(() => {});
158
+ try {
159
+ const proof = job ? settlementProof({ shellReceipt: result.shellReceipt, installResult: result.installResult }, job.identity) : null;
160
+ result.executionReceipt = proof;
161
+ if (proof) installOwnership?.settle(proof);
162
+ if (proof) {
163
+ try { await job.acknowledge(result); }
164
+ catch (error) { result.cleanupError = error.code || 'install_cleanup_failed'; }
165
+ }
166
+ installation.phase = 'verifying';
167
+ installation.process = result;
168
+ const receipt = await persist('action-receipt', { actionId: `${operationId}:install`, startedAtMs: result.startedAtMs,
169
+ completedAtMs: result.completedAtMs, mechanicalStatus: proof?.requestSucceeded ? 'ok' : 'failed',
170
+ ambiguous: job ? !proof : false, dispatched: proof?.dispatched ?? (job ? null : false),
171
+ settled: job ? Boolean(proof) : true, executionReceipt: proof,
172
+ action: { action: 'install-apk', sha256: artifact.sha256 }, providerResult: result });
173
+ installation.receiptId = receipt.evidenceId;
174
+ installation.after = await (dependencies.installedIdentity || installedIdentity)(args, artifact);
175
+ installation.requestSucceeded = proof?.requestSucceeded === true;
176
+ installation.verified = installation.requestSucceeded && installation.after.identityMatches === true;
177
+ const status = cancelled ? 'cancelled' : uiFailure ? uiFailure.status : result.timedOut ? 'timeout'
178
+ : installation.verified ? 'completed' : job && !proof || installation.after.known === false ? 'ambiguous' : 'failed';
179
+ const error = status === 'completed' ? null : status === 'cancelled' ? 'installation_cancelled'
180
+ : uiFailure ? uiFailure.error : status === 'timeout' ? 'installation_timeout' : job && !proof ? 'install_completion_unavailable'
181
+ : !installation.requestSucceeded ? 'package_install_failed' : 'installed_apk_identity_mismatch';
182
+ installation.phase = status;
183
+ const checked = await persist('checkpoint', { stepId: 'installed-identity', payloadSummary: { status, error, installation } });
184
+ installation.verificationEvidenceId = checked.evidenceId;
185
+ worker.runtime.state.status = status; worker.runtime.state.error = error;
186
+ worker.runtime.emit('installation_verified', { verified: installation.verified, status, evidenceId: checked.evidenceId });
187
+ } catch (error) {
188
+ installation.phase = 'blocked_evidence_store';
189
+ worker.runtime.state.status = 'blocked_evidence_store'; worker.runtime.state.error = error.code || 'installation_evidence_failed';
190
+ } finally { final = true; release(); }
191
+ }
192
+ async function submit() {
193
+ try {
194
+ installation.before = await (dependencies.installedIdentity || installedIdentity)(args, artifact);
195
+ if (cancelled || uiFailure) return snapshot();
196
+ await persist('plan', { planStepId: 'submit-install', actionSpecHash: checksumOf(artifact), action: { action: 'install-apk', artifact }, payloadSummary: { artifact, before: installation.before } });
197
+ if (cancelled || uiFailure) return snapshot();
198
+ job = await (dependencies.prepareJob || prepareInstallJob)(args, artifact, `${operationId}:install`);
199
+ await persist('dispatch-marker', { planStepId: 'submit-install', actionId: `${operationId}:install`, actionSpecHash: checksumOf(artifact),
200
+ action: { action: 'install-apk', sha256: artifact.sha256 }, executionIdentity: job.identity, state: 'prepared' });
201
+ installOwnership = ownership.retain(job.identity);
202
+ installation.phase = 'installing'; worker.runtime.state.status = 'waiting_for_observation';
203
+ worker.runtime.emit('installation_submitted', { sha256: artifact.sha256 });
204
+ finished = job.done.then(finalize);
205
+ if (cancelled || uiFailure) job.cancel(); else job.start();
206
+ return snapshot();
207
+ } catch (error) {
208
+ if (job && !finished) {
209
+ // Failed evidence/ownership admission cannot launch installation. Obtain
210
+ // the original not-admitted receipt before cleaning its private APK.
211
+ const stopped = await job.cancel();
212
+ if (settlementProof({ shellReceipt: stopped.shellReceipt, installResult: stopped.installResult }, job.identity)) {
213
+ try { await job.acknowledge(stopped); } catch (cleanup) { error.cleanupError = cleanup.code || 'install_cleanup_failed'; }
214
+ }
215
+ }
216
+ final = true; release(); throw error;
217
+ }
218
+ }
219
+ function start() {
220
+ if (starting || final || cancelled || uiFailure) return Promise.resolve({ ...snapshot(), ok: false, error: 'operation_stopped' });
221
+ starting = submit();
222
+ return starting;
223
+ }
224
+ async function stop(status, error) {
225
+ if (final) return snapshot();
226
+ if (status === 'cancelled') cancelled = true;
227
+ else uiFailure = { status, error };
228
+ const uiStopped = status === 'cancelled' ? worker.cancel() : worker.intervene(error);
229
+ job?.cancel();
230
+ await uiStopped;
231
+ await starting?.catch(() => {});
232
+ if (final) return snapshot();
233
+ if (!job && !finished) finished = finalize({ code: null, cancelled: true, timedOut: false, dispatched: false,
234
+ stdout: '', stderr: '', startedAtMs: Date.now(), completedAtMs: Date.now() });
235
+ await finished;
236
+ return snapshot();
237
+ }
238
+ async function ui(action) {
239
+ if (final || finalizing) return snapshot();
240
+ if (activeUi) return { ...snapshot(), ok: false, error: 'operation_busy' };
241
+ activeUi = ownership.run(action);
242
+ let result;
243
+ try { result = await activeUi; } finally { activeUi = null; }
244
+ if (['failed', 'ambiguous', 'blocked_evidence_store'].includes(worker.runtime.state.status)) {
245
+ uiFailure = { status: worker.runtime.state.status, error: worker.runtime.state.error };
246
+ installation.uiFailure = uiFailure;
247
+ job.cancel();
248
+ }
249
+ if (finalizing || uiFailure) { await finished; return snapshot(); }
250
+ return { ...snapshot(), ok: result.ok, error: result.error };
251
+ }
252
+ return {
253
+ ...worker, isManagedWorkflow: true, isFinished: () => final, start, status: snapshot,
254
+ observe: args => ui(() => worker.observe(args)),
255
+ decide: decision => {
256
+ if (decision?.agentDecision !== 'act') return Promise.resolve({ ...snapshot(), ok: false,
257
+ error: 'installation_requires_package_verification', message: 'Installation completion is determined by the APK verifier. Use intent cancel to stop.' });
258
+ return ui(() => worker.decide(decision));
259
+ },
260
+ cancel: () => stop('cancelled'),
261
+ pause() { worker.pause(); return snapshot(); },
262
+ resume() { worker.resume(); return snapshot(); },
263
+ intervene: reason => stop('intervention_required', reason),
264
+ };
265
+ }
266
+
267
+ module.exports = { inspectApk, prepareApk, installedIdentity, installerDeviceAdapter, createInstallIntent };
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const { getProcessDeviceMutationLease, runDeviceEffect } = require('../shared-kernel/device-mutation-lease');
4
+ const { executionFields } = require('../command-errors');
5
+ const { checksumOf } = require('../shared-kernel/evidence-schema');
6
+ const { intentError } = require('./intent-errors');
7
+ const { checkExecution } = require('../shared-kernel/execution-scope');
8
+ const { admitExecutionMutation } = require('../shared-kernel/execution-admission');
9
+
10
+ async function executeDecisionAction(context, decision) {
11
+ checkExecution();
12
+ const exposed = context.store.canExposeRevision(context.operationId);
13
+ if (!exposed.ok) return intentError(exposed.error);
14
+ if (decision.basedOnRevision !== exposed.revision) {
15
+ return intentError('reobserve_required', { revision: exposed.revision });
16
+ }
17
+ const persistedDecision = await context.store.persist('decision', {
18
+ decisionId: decision.decisionId,
19
+ operationId: context.operationId,
20
+ revision: exposed.revision,
21
+ basedOnEvidenceIds: [context.latestEvidenceId].filter(Boolean),
22
+ actionSpecHash: specHash(decision.action),
23
+ agentDecision: 'act',
24
+ mode: context.mode,
25
+ action: decision.action,
26
+ ...(context.latestRoute ? { foreground: context.latestRoute } : {}),
27
+ reason: decision.reason,
28
+ timestampMs: context.now(),
29
+ target: context.target,
30
+ });
31
+ if (!persistedDecision.ok) {
32
+ return intentError(persistedDecision.error || 'plan_or_decision_not_persisted');
33
+ }
34
+ context.latestEvidenceIds.decision = persistedDecision.evidenceId;
35
+ checkExecution();
36
+ if (context.canDispatch && !context.canDispatch()) return intentError('cancelled');
37
+ const timings = context.timings || {};
38
+ const startedAtMs = context.now();
39
+ try {
40
+ return await admitExecutionMutation(context.target, getProcessDeviceMutationLease(), async () => {
41
+ const marker = await context.store.persist('dispatch-marker', {
42
+ actionId: `${context.operationId}:${decision.decisionId}`,
43
+ operationId: context.operationId,
44
+ revision: exposed.revision,
45
+ decisionId: decision.decisionId,
46
+ target: context.target,
47
+ actionSpecHash: specHash(decision.action),
48
+ action: decision.action,
49
+ ...(context.latestRoute ? { foreground: context.latestRoute } : {}),
50
+ state: 'prepared',
51
+ timestampMs: context.now(),
52
+ });
53
+ if (!marker.ok) return intentError(marker.error || 'dispatch_marker_not_persisted');
54
+ context.latestEvidenceIds.marker = marker.evidenceId;
55
+ context.actionId = `${context.operationId}:${decision.decisionId}`;
56
+ if (!context.store.canDispatch(context.operationId).ok) {
57
+ return intentError('dispatch_marker_not_persisted');
58
+ }
59
+ let result;
60
+ try {
61
+ checkExecution();
62
+ result = context.canDispatch && !context.canDispatch()
63
+ ? { ok: false, mechanicalStatus: 'failed', error: 'cancelled', ambiguous: false, dispatched: false }
64
+ : await runDeviceEffect({ kind: 'intent-provider', actionId: context.actionId, target: context.target }, () => context.adapter.action({
65
+ ...context.target,
66
+ actionId: context.actionId,
67
+ route: context.latestRoute,
68
+ primaryProvider: context.provider,
69
+ spec: {
70
+ ...decision.action,
71
+ provider: decision.action?.provider || context.latestProvider || context.provider || null,
72
+ },
73
+ rawTree: context.latestRawTree,
74
+ }), result => result?.ok === true || result?.settled === true
75
+ || result?.dispatched === false && result?.ambiguous !== true, { allowNested: true });
76
+ } catch (error) {
77
+ result = { ok: false, mechanicalStatus: 'failed', error: error?.code || error?.message || String(error),
78
+ ambiguous: error?.ambiguous ?? true, dispatched: error?.dispatched ?? null, ...executionFields(error) };
79
+ }
80
+ if (!result || typeof result.ok !== 'boolean') {
81
+ result = { ok: false, mechanicalStatus: 'failed', error: 'invalid_action_receipt', ambiguous: true };
82
+ }
83
+ if (result?.targetLeaseWaitMs != null) {
84
+ timings.targetLeaseWaitMs = (timings.targetLeaseWaitMs || 0) + result.targetLeaseWaitMs;
85
+ }
86
+ const completedAtMs = context.now();
87
+ timings.actionMs = (timings.actionMs || 0) + (completedAtMs - startedAtMs);
88
+ const receiptStarted = context.now();
89
+ const receipt = await context.store.persist('action-receipt', {
90
+ actionId: `${context.operationId}:${decision.decisionId}`,
91
+ operationId: context.operationId,
92
+ startedAtMs,
93
+ completedAtMs,
94
+ mechanicalStatus: result.ok ? 'ok' : 'failed',
95
+ providerResult: result || null,
96
+ error: result?.error || null,
97
+ ambiguous: Boolean(result?.ambiguous),
98
+ dispatched: result.dispatched ?? (result.ok ? true : null),
99
+ executionReceipt: result.executionReceipt ?? null,
100
+ action: decision.action,
101
+ ...(context.latestRoute ? { foreground: context.latestRoute } : {}),
102
+ resolved: result?.resolved,
103
+ matched: result?.matched,
104
+ rawTreeId: context.latestRawTreeId,
105
+ basedOnEvidenceIds: [context.latestEvidenceId].filter(Boolean),
106
+ parentFactId: context.latestEvidenceId,
107
+ revision: exposed.revision,
108
+ target: context.target,
109
+ });
110
+ timings.receiptCommitMs = (timings.receiptCommitMs || 0) + (context.now() - receiptStarted);
111
+ context.lastAction = { actionId: context.actionId, receiptId: receipt.ok ? receipt.evidenceId : null,
112
+ error: result.error || null, ambiguous: result.ambiguous === true || !receipt.ok,
113
+ dispatched: result.dispatched ?? (result.ok ? true : null), mechanicalStatus: result.ok ? 'ok' : 'failed' };
114
+ if (!receipt.ok) {
115
+ return intentError(receipt.error || 'receipt_not_persisted', { ambiguous: true });
116
+ }
117
+ context.latestEvidenceIds.receipt = receipt.evidenceId;
118
+ if (result?.ambiguous) return intentError('ambiguous', { ambiguous: true });
119
+ if (result?.ok === false) return intentError(result.error || 'action_failed');
120
+ return { ok: true, receiptId: receipt.evidenceId, decisionId: decision.decisionId };
121
+ });
122
+ } catch (error) { return intentError(error.code || error.message, executionFields(error)); }
123
+ }
124
+
125
+ function specHash(action = {}) {
126
+ return checksumOf(action);
127
+ }
128
+
129
+ module.exports = { executeDecisionAction };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ function createAutonomousAgentAdapter({ decide } = {}) {
4
+ if (typeof decide !== 'function') {
5
+ throw new TypeError('AutonomousAgentAdapter requires a decide function from the Agent');
6
+ }
7
+ let calls = 0;
8
+ return {
9
+ get callCount() { return calls; },
10
+ async decide(input) {
11
+ calls += 1;
12
+ return decide(input);
13
+ },
14
+ };
15
+ }
16
+
17
+ function createIntentBudget({
18
+ maxSteps = 30,
19
+ maxDurationMs = 120000,
20
+ maxAgentCalls = 30,
21
+ allowlist = ['tap'],
22
+ startedAtMs,
23
+ now = Date.now,
24
+ } = {}) {
25
+ const started = startedAtMs != null ? startedAtMs : now();
26
+ return {
27
+ maxSteps,
28
+ maxDurationMs,
29
+ maxAgentCalls,
30
+ allowlist,
31
+ check({ steps = 0, agentCalls = 0, action = null, atMs = now(), phase = 'agent' } = {}) {
32
+ if ((phase === 'agent' || action) && steps >= maxSteps) return { ok: false, reason: 'max_steps' };
33
+ if (atMs - started >= maxDurationMs) return { ok: false, reason: 'max_duration' };
34
+ if (phase === 'agent' ? agentCalls >= maxAgentCalls : agentCalls > maxAgentCalls) return { ok: false, reason: 'max_agent_calls' };
35
+ if (action && !allowlist.includes(action.action)) {
36
+ return { ok: false, reason: 'action_not_allowed' };
37
+ }
38
+ return { ok: true };
39
+ },
40
+ };
41
+ }
42
+
43
+ module.exports = {
44
+ createAutonomousAgentAdapter,
45
+ createIntentBudget,
46
+ };
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const { capturePage, unavailablePage } = require('../shared-kernel/live-capture-query');
4
+
5
+ function createIntentCapturePort({ query } = {}) {
6
+ if (typeof query !== 'function') {
7
+ throw new TypeError('query');
8
+ }
9
+
10
+ async function observe(requirements = {}, executionContext = {}) {
11
+ const stream = requirements.stream;
12
+ if (!stream) {
13
+ return unavailablePage('unsupported_capture_stream');
14
+ }
15
+ if (executionContext.runtimeEpochChanged === true || executionContext.disconnected === true) {
16
+ return unavailablePage(executionContext.disconnected ? 'target_disconnected' : 'runtime_epoch_changed');
17
+ }
18
+ // Foreground routing belongs to Intent observation, not a phone data query.
19
+ const { foregroundPackages: _foregroundPackages, ...target } = executionContext.target || {};
20
+ for (const key of ['serial', 'packageName', 'port', 'adb', 'platform', 'deviceId', 'bundleId', 'sessionId', 'targetId', 'runtimeUrl', 'iosHost', 'iosPort']) {
21
+ if (target[key] != null && requirements[key] != null && requirements[key] !== target[key]) {
22
+ return unavailablePage('capture_target_mismatch');
23
+ }
24
+ }
25
+ if (target.platform === 'web' && requirements.runtimeEpoch !== undefined
26
+ && requirements.runtimeEpoch !== target.runtimeEpoch) return unavailablePage('capture_target_mismatch');
27
+ const page = await query({
28
+ ...requirements,
29
+ ...target,
30
+ view: requirements.view ?? (requirements.history === true ? 'connected-history' : 'decision-window'),
31
+ platform: executionContext.platform ?? target.platform ?? requirements.platform,
32
+ stream,
33
+ afterActionId: afterActionIdOf(requirements, executionContext),
34
+ timeoutMs: timeoutMsOf(requirements, executionContext),
35
+ runtimeEpoch: executionContext.runtimeEpoch ?? target.runtimeEpoch ?? requirements.runtimeEpoch ?? null,
36
+ sinceMs: requirements.sinceMs ?? null,
37
+ limit: requirements.limit ?? ((executionContext.platform ?? target.platform ?? requirements.platform) === 'web' ? 16 : 200),
38
+ });
39
+ return capturePage(page);
40
+ }
41
+
42
+ return { observe };
43
+ }
44
+
45
+ function afterActionIdOf(requirements, executionContext) {
46
+ if (requirements.afterActionId === null) return null;
47
+ if (executionContext.evidenceWindow && executionContext.evidenceWindow.afterActionId != null) {
48
+ return executionContext.evidenceWindow.afterActionId;
49
+ }
50
+ if (executionContext.actionId != null) return executionContext.actionId;
51
+ if (requirements.afterActionId != null) return requirements.afterActionId;
52
+ return null;
53
+ }
54
+
55
+ function timeoutMsOf(requirements, executionContext) {
56
+ if (executionContext.evidenceWindow && executionContext.evidenceWindow.timeoutMs != null) {
57
+ return executionContext.evidenceWindow.timeoutMs;
58
+ }
59
+ if (executionContext.timeoutMs != null) return executionContext.timeoutMs;
60
+ if (requirements.timeoutMs != null) return requirements.timeoutMs;
61
+ return null;
62
+ }
63
+
64
+ module.exports = { createIntentCapturePort };