@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,44 @@
1
+ 'use strict';
2
+
3
+ const { object, text, validateValue } = require('./argument-schema');
4
+ const schema = 'aab.web-dom-target/v1';
5
+ const string = { type: 'string' };
6
+ const elementKeys = ['elementId', 'tag', 'id', 'name', 'type', 'role', 'ariaLabel', 'placeholder', 'href', 'text'];
7
+ const elementSchema = object(Object.fromEntries(elementKeys.map(key => [key, key === 'elementId' ? text : string])), elementKeys);
8
+ const pageSchema = object({ schemaVersion: { const: schema }, sessionId: text, runtimeEpoch: text,
9
+ targetId: text, navigationId: text, url: text }, ['schemaVersion', 'sessionId', 'runtimeEpoch', 'targetId', 'navigationId', 'url']);
10
+ const selectorSchema = { ...object({ elementId: text, text, ariaLabel: text, css: text, tag: text, role: text }),
11
+ oneOf: ['elementId', 'text', 'ariaLabel', 'css'].map(key => ({ required: [key] })) };
12
+ const intentSelectorSchema = { ...object({ elementId: text, text, ariaLabel: text, tag: text, role: text }),
13
+ oneOf: ['elementId', 'text', 'ariaLabel'].map(key => ({ required: [key] })) };
14
+ const expectedTargetSchema = object({ pageRef: pageSchema, element: elementSchema }, ['pageRef', 'element']);
15
+ const reject = error => ({ ok: false, error, dispatched: false, ambiguous: false });
16
+
17
+ function validateSnapshot(dom, target) {
18
+ validateValue(dom.pageRef, pageSchema);
19
+ if (dom.targetSchema !== schema || dom.url !== dom.pageRef.url
20
+ || ['sessionId', 'runtimeEpoch', 'targetId'].some(key => dom.pageRef[key] !== target[key])
21
+ || !Array.isArray(dom.controls) || dom.controlCount !== dom.controls.length
22
+ || typeof dom.truncated !== 'boolean' || typeof dom.bodyTextTruncated !== 'boolean') throw new Error('invalid_web_dom_target');
23
+ const ids = new Set();
24
+ for (const node of dom.controls) {
25
+ validateValue(Object.fromEntries(elementKeys.map(key => [key, node[key]])), elementSchema);
26
+ if (ids.has(node.elementId) || ['visible', 'disabled', 'editable'].some(key => typeof node[key] !== 'boolean')
27
+ || ![true, false, 'mixed', null].includes(node.checked)) throw new Error('invalid_web_dom_control');
28
+ ids.add(node.elementId);
29
+ }
30
+ }
31
+
32
+ function selectWebNode(tree, selector) {
33
+ if (tree?.webTargetSchema !== schema || tree.dom?.targetSchema !== schema) return reject('web_dom_target_schema_required');
34
+ if (selector.css !== undefined) return reject('web_intent_css_selector_requires_observation');
35
+ if (tree.dom.truncated) return reject('web_dom_snapshot_truncated');
36
+ const matches = tree.dom.controls.filter(node => node.visible === true
37
+ && ['elementId', 'text', 'ariaLabel', 'tag', 'role'].every(key => selector[key] === undefined || selector[key] === node[key]));
38
+ if (matches.length !== 1) return { ...reject(matches.length ? 'web_element_ambiguous' : 'web_element_not_found'), matched: matches.length };
39
+ const node = matches[0];
40
+ if (node.disabled) return reject('web_element_disabled');
41
+ return { ok: true, node, targetRef: { pageRef: tree.pageRef, element: Object.fromEntries(elementKeys.map(key => [key, node[key]])) } };
42
+ }
43
+
44
+ module.exports = { schema, selectorSchema, intentSelectorSchema, expectedTargetSchema, pageSchema, elementKeys, selectWebNode, validateSnapshot };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ // Decode one XML layer. In particular, 
 is the literal text 
.
4
+ function decodeXmlAttribute(value) {
5
+ const named = { quot: '"', apos: "'", lt: '<', gt: '>', amp: '&' };
6
+ return value.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|quot|apos|lt|gt|amp);/g, (_, entity) => {
7
+ if (entity[0] !== '#') return named[entity];
8
+ const code = entity[1] === 'x' ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10);
9
+ if (!(code === 9 || code === 10 || code === 13 || (code >= 0x20 && code <= 0xD7FF)
10
+ || (code >= 0xE000 && code <= 0xFFFD) || (code >= 0x10000 && code <= 0x10FFFF))) {
11
+ throw new Error('invalid_xml_character_reference');
12
+ }
13
+ return String.fromCodePoint(code);
14
+ });
15
+ }
16
+
17
+ function parseXmlAttributes(tag) {
18
+ const attributes = {};
19
+ for (const match of tag.matchAll(/([A-Za-z0-9_:-]+)="([^"]*)"/g)) {
20
+ attributes[match[1]] = decodeXmlAttribute(match[2]);
21
+ }
22
+ return attributes;
23
+ }
24
+
25
+ module.exports = { decodeXmlAttribute, parseXmlAttributes };
@@ -0,0 +1,275 @@
1
+ 'use strict';
2
+
3
+ const { createHash } = require('node:crypto');
4
+ const { targetIdentity } = require('./shared-kernel/execution-target');
5
+ const { CommandError } = require('./command-errors');
6
+ const { isAndroidMutation, isMutationCommand, executionTimeoutMs } = require('./command-registry');
7
+ const { runExecution, checkExecution, currentExecution, executionFailure } = require('./shared-kernel/execution-scope');
8
+ const { getProcessDeviceMutationLease } = require('./shared-kernel/device-mutation-lease');
9
+
10
+ class TargetExecution {
11
+ constructor({ requestTtlMs = 5 * 60_000, maxRequests = 2_048, now = Date.now } = {}) {
12
+ if (!Number.isFinite(requestTtlMs) || requestTtlMs < 0) {
13
+ throw new TypeError('requestTtlMs must be a non-negative number');
14
+ }
15
+ if (!Number.isInteger(maxRequests) || maxRequests < 1) {
16
+ throw new TypeError('maxRequests must be a positive integer');
17
+ }
18
+ if (typeof now !== 'function') {
19
+ throw new TypeError('now must be a function');
20
+ }
21
+ this.targetTails = new Map();
22
+ this.requests = new Map();
23
+ this.requestTtlMs = requestTtlMs;
24
+ this.maxRequests = maxRequests;
25
+ this.now = now;
26
+ }
27
+
28
+ execute(command, args, runner) {
29
+ if (typeof command !== 'string' || command.length === 0) {
30
+ throw new TypeError('command is required');
31
+ }
32
+ if (!args || typeof args !== 'object' || Array.isArray(args)) {
33
+ throw new TypeError('args must be an object');
34
+ }
35
+ if (typeof runner !== 'function') {
36
+ throw new TypeError('runner must be a function');
37
+ }
38
+
39
+ const normalizedArgs = { ...args };
40
+ const target = targetFor(command, normalizedArgs);
41
+ const requestKey = idempotencyKey(target.key, normalizedArgs.requestId);
42
+ const requestHash = requestKey === null ? null : requestDigest(command, normalizedArgs);
43
+ this.pruneRequests();
44
+ if (requestKey !== null && this.requests.has(requestKey)) {
45
+ const existing = this.requests.get(requestKey);
46
+ if (existing.requestHash !== requestHash) throw new CommandError('idempotency_conflict',
47
+ 'requestId was already used for a different command or arguments.', { field: 'requestId' });
48
+ return existing.promise;
49
+ }
50
+
51
+ const requestedAtMs = this.now();
52
+ const deviceMutation = isAndroidMutation(command, normalizedArgs);
53
+ const queueKey = deviceMutation ? `android-device:${normalizedArgs.serial}`
54
+ : command === 'web-execution' ? `${target.key}:control`
55
+ : command === 'web-dom' ? `${target.key}:observation` : target.key;
56
+ const execution = runExecution({ timeoutMs: executionTimeoutMs(command, normalizedArgs), mutation: isMutationCommand(command, normalizedArgs) }, () => this.enqueue(queueKey, async () => {
57
+ const startedAtMs = this.now();
58
+ try {
59
+ const legacyResult = deviceMutation
60
+ ? await getProcessDeviceMutationLease().run(normalizedArgs.serial, () => runner(command, normalizedArgs))
61
+ : await runner(command, normalizedArgs);
62
+ const completedAtMs = this.now();
63
+ const feedback = buildFeedback({
64
+ command,
65
+ requestId: normalizedArgs.requestId,
66
+ target,
67
+ status: resultStatus(legacyResult),
68
+ requestedAtMs,
69
+ startedAtMs,
70
+ completedAtMs,
71
+ });
72
+ return feedbackEnabled(normalizedArgs.feedback)
73
+ ? appendFeedback(legacyResult, feedback)
74
+ : legacyResult;
75
+ } catch (error) {
76
+ const completedAtMs = this.now();
77
+ if (feedbackEnabled(normalizedArgs.feedback)) {
78
+ error._feedback = buildFeedback({
79
+ command,
80
+ requestId: normalizedArgs.requestId,
81
+ target,
82
+ status: 'failed',
83
+ requestedAtMs,
84
+ startedAtMs,
85
+ completedAtMs,
86
+ });
87
+ }
88
+ throw error;
89
+ }
90
+ }));
91
+
92
+ if (requestKey !== null) {
93
+ const request = {
94
+ requestHash,
95
+ promise: execution,
96
+ settled: false,
97
+ expiresAtMs: Number.POSITIVE_INFINITY,
98
+ };
99
+ this.requests.set(requestKey, request);
100
+ const markSettled = () => {
101
+ request.settled = true;
102
+ request.expiresAtMs = this.now() + this.requestTtlMs;
103
+ this.pruneRequests();
104
+ };
105
+ execution.then(markSettled, markSettled);
106
+ }
107
+ return execution;
108
+ }
109
+
110
+ pruneRequests() {
111
+ const now = this.now();
112
+ for (const [key, request] of this.requests) {
113
+ if (request.settled && request.expiresAtMs < now) {
114
+ this.requests.delete(key);
115
+ }
116
+ }
117
+ if (this.requests.size <= this.maxRequests) return;
118
+ for (const [key, request] of this.requests) {
119
+ if (!request.settled) continue;
120
+ this.requests.delete(key);
121
+ if (this.requests.size <= this.maxRequests) return;
122
+ }
123
+ }
124
+
125
+ enqueue(targetKey, action) {
126
+ const previous = this.targetTails.get(targetKey) || Promise.resolve();
127
+ const scope = currentExecution();
128
+ let started = false;
129
+ let abort;
130
+ const execution = previous.catch(() => undefined).then(() => {
131
+ started = true;
132
+ scope?.signal.removeEventListener('abort', abort);
133
+ checkExecution();
134
+ return action();
135
+ });
136
+ this.targetTails.set(targetKey, execution);
137
+ const clearTail = () => {
138
+ if (this.targetTails.get(targetKey) === execution) {
139
+ this.targetTails.delete(targetKey);
140
+ }
141
+ };
142
+ execution.then(clearTail, clearTail);
143
+ // A queued cancellation may return immediately, but the queue barrier stays
144
+ // behind the preceding operation. Once dispatched, await actual settlement.
145
+ return new Promise((resolve, reject) => {
146
+ abort = () => { if (!started) reject(executionFailure(scope)); };
147
+ scope?.signal.addEventListener('abort', abort, { once: true });
148
+ execution.then(resolve, reject).finally(() => scope?.signal.removeEventListener('abort', abort));
149
+ });
150
+ }
151
+ }
152
+
153
+ function feedbackEnabled(value) {
154
+ return value !== false && String(value || 'auto').toLowerCase() !== 'off';
155
+ }
156
+
157
+ function requestDigest(command, args) {
158
+ function canonical(value) {
159
+ if (Array.isArray(value)) return value.map(canonical);
160
+ if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])]));
161
+ return value;
162
+ }
163
+ const { requestId, ...content } = args;
164
+ return createHash('sha256').update(JSON.stringify(canonical({ command, arguments: content }))).digest('hex');
165
+ }
166
+
167
+ function targetFor(command, args) {
168
+ if (command === 'logcat' && String(args.deviceLogScope || '').toLowerCase() === 'device') {
169
+ const serial = targetPart(args.serial);
170
+ return {
171
+ kind: 'android-device',
172
+ key: targetKey('android-device', serial, ''),
173
+ serial,
174
+ };
175
+ }
176
+
177
+ if (command.startsWith('ios-')) {
178
+ const deviceId = targetPart(args.deviceId);
179
+ const bundleId = targetPart(args.bundleId);
180
+ return {
181
+ kind: 'ios', platform: 'ios',
182
+ key: targetIdentity({ platform: 'ios', deviceId, bundleId }),
183
+ deviceId,
184
+ bundleId,
185
+ };
186
+ }
187
+
188
+ if (command.startsWith('web-')) {
189
+ const sessionId = targetPart(args.sessionId);
190
+ const targetId = targetPart(args.targetId);
191
+ const runtimeEpoch = targetPart(args.runtimeEpoch);
192
+ return {
193
+ kind: 'web', platform: 'web',
194
+ key: targetIdentity({ platform: 'web', sessionId, runtimeEpoch, targetId }),
195
+ sessionId,
196
+ runtimeEpoch,
197
+ targetId,
198
+ };
199
+ }
200
+
201
+ const serial = targetPart(args.serial);
202
+ const packageName = targetPart(args.packageName);
203
+ return {
204
+ kind: 'android', platform: 'android',
205
+ key: targetIdentity({ platform: 'android', serial, packageName }),
206
+ serial,
207
+ packageName,
208
+ };
209
+ }
210
+
211
+ function targetPart(value) {
212
+ return value === undefined || value === null ? '' : String(value);
213
+ }
214
+
215
+ function targetKey(kind, first, second) {
216
+ return `${kind}:${JSON.stringify([first, second])}`;
217
+ }
218
+
219
+ function idempotencyKey(targetKeyValue, requestId) {
220
+ if (requestId === undefined || requestId === null) return null;
221
+ return JSON.stringify([targetKeyValue, String(requestId)]);
222
+ }
223
+
224
+ function resultStatus(result) {
225
+ if (result && typeof result === 'object' && result.ok === false) return 'failed';
226
+ if (result && typeof result === 'object' && result.verified === true) return 'verified';
227
+ if (result === undefined || result === null) return 'inconclusive';
228
+ if (result && typeof result === 'object' && result.inconclusive === true) return 'inconclusive';
229
+ return 'completed';
230
+ }
231
+
232
+ function buildFeedback({
233
+ command,
234
+ requestId,
235
+ target,
236
+ status,
237
+ requestedAtMs,
238
+ startedAtMs,
239
+ completedAtMs,
240
+ }) {
241
+ return {
242
+ status,
243
+ target,
244
+ dispatch: {
245
+ command,
246
+ requestId: requestId === undefined ? null : requestId,
247
+ serializedByTarget: true,
248
+ },
249
+ timings: {
250
+ requestedAtMs,
251
+ startedAtMs,
252
+ completedAtMs,
253
+ queueWaitMs: startedAtMs - requestedAtMs,
254
+ durationMs: completedAtMs - startedAtMs,
255
+ },
256
+ evidence: [],
257
+ };
258
+ }
259
+
260
+ function appendFeedback(legacyResult, feedback) {
261
+ if (
262
+ legacyResult === null
263
+ || typeof legacyResult !== 'object'
264
+ || Buffer.isBuffer(legacyResult)
265
+ || Array.isArray(legacyResult)
266
+ ) {
267
+ return legacyResult;
268
+ }
269
+ return { ...legacyResult, _feedback: feedback };
270
+ }
271
+
272
+ module.exports = {
273
+ TargetExecution,
274
+ targetFor,
275
+ };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+ const { webCommandSchema } = require('../shared-kernel/provider-command-contracts');
3
+ const { selectorSchema, expectedTargetSchema } = require('../shared-kernel/web-dom-target');
4
+ const text = { type: 'string', minLength: 1, maxLength: 1024 };
5
+ const integer = (minimum, maximum) => ({ type: 'integer', minimum, maximum });
6
+ const object = (properties, required = []) => ({ type: 'object', additionalProperties: false, properties, required });
7
+ const target = { sessionId: text, runtimeEpoch: text, targetId: { enum: ['main'], default: 'main' } };
8
+ const control = { timeoutMs: { ...integer(1, 30000), default: 5000 }, requestId: text, feedback: { enum: ['auto', 'off', 'full'] } };
9
+ const query = { history: { type: 'boolean', default: false }, sinceMs: integer(0, Number.MAX_SAFE_INTEGER),
10
+ cursor: { ...text, maxLength: 4096 }, throughCursor: { ...text, maxLength: 4096 }, limit: { ...integer(1, 16), default: 16 } };
11
+ const cursorPair = { anyOf: [{ required: ['cursor', 'throughCursor'] }, { not: { anyOf: [{ required: ['cursor'] }, { required: ['throughCursor'] }] } }] };
12
+
13
+ function webSchema(command) {
14
+ if (command === 'web-session-start') return object({ host: text, webPort: integer(0, 65535),
15
+ path: { ...text, pattern: '^/[A-Za-z0-9/_-]+$' }, token: text, ...control });
16
+ if (['web-provider-status', 'web-session-stop', 'web-connect-info', 'web-sessions'].includes(command)) return object(control);
17
+ if (command === 'web-status') return object({ sessionId: text, ...control }, ['sessionId']);
18
+ if (command === 'web-execution') return { ...object({ ...target, ...control,
19
+ operation: { enum: ['status', 'result', 'cancel', 'reconcile'] }, actionId: text }, ['operation', 'sessionId', 'runtimeEpoch']),
20
+ oneOf: [{ properties: { operation: { enum: ['status', 'reconcile'] }, actionId: false } },
21
+ { properties: { operation: { enum: ['result', 'cancel'] } }, required: ['actionId'] }] };
22
+ if (command === 'web-command') {
23
+ const schema = webCommandSchema({ ...target, ...control });
24
+ for (const branch of schema.anyOf) branch.required.push('runtimeEpoch');
25
+ schema.required.push('sessionId', 'runtimeEpoch');
26
+ return schema;
27
+ }
28
+ if (['web-logs', 'web-network', 'web-state', 'web-events'].includes(command)) return {
29
+ ...object({ ...target, ...query, ...control,
30
+ view: { enum: ['decision-window', 'connected-history'], default: 'decision-window' },
31
+ factCursor: { ...text, maxLength: 4096 }, afterActionId: text,
32
+ }, ['sessionId', 'runtimeEpoch']), allOf: [cursorPair,
33
+ { not: { properties: { history: { const: true }, view: { const: 'decision-window' } }, required: ['history', 'view'] } },
34
+ { if: { required: ['afterActionId'] }, then: { required: ['factCursor'] } },
35
+ { if: { anyOf: [
36
+ { properties: { history: { const: true } }, required: ['history'] },
37
+ { properties: { view: { const: 'connected-history' } }, required: ['view'] },
38
+ ] }, then: { properties: { factCursor: false, afterActionId: false } } },
39
+ ] };
40
+ if (command === 'web-dom') return { ...object({ ...target, ...query, ...control, selector: text, maxControls: integer(1, 1000) }, ['sessionId', 'runtimeEpoch']),
41
+ allOf: [cursorPair], oneOf: [
42
+ { properties: { history: { const: false }, cursor: false, throughCursor: false, sinceMs: false, limit: false } },
43
+ { properties: { history: { const: true }, selector: false, maxControls: false }, required: ['history'] },
44
+ ] };
45
+ if (command === 'web-scroll') return { ...object({ ...target, ...control, selector: selectorSchema, expectedTarget: expectedTargetSchema,
46
+ mode: { enum: ['into-view', 'by'] }, deltaX: { type: 'number' }, deltaY: { type: 'number' } }, ['sessionId', 'runtimeEpoch', 'mode']), oneOf: [
47
+ { properties: { mode: { const: 'into-view' }, deltaX: false, deltaY: false }, required: ['selector'] },
48
+ { properties: { mode: { const: 'by' } }, anyOf: [{ required: ['deltaX'] }, { required: ['deltaY'] }] },
49
+ ], allOf: [{ if: { required: ['expectedTarget'] }, then: { required: ['selector'] } }] };
50
+ const fields = command === 'web-key' ? { selector: selectorSchema, expectedTarget: expectedTargetSchema, key: { enum: ['Enter', 'Escape'] } }
51
+ : command === 'web-input' ? { selector: selectorSchema, expectedTarget: expectedTargetSchema, value: { type: 'string', maxLength: 16384 } }
52
+ : command === 'web-click' ? { selector: selectorSchema, expectedTarget: expectedTargetSchema }
53
+ : { selector: selectorSchema, targetText: text };
54
+ const schema = object({ ...target, ...control, ...fields }, ['sessionId', 'runtimeEpoch',
55
+ ...(command === 'web-key' ? ['selector', 'key'] : command === 'web-input' ? ['selector', 'value'] : command === 'web-click' ? ['selector'] : [])]);
56
+ if (command === 'web-wait') schema.oneOf = [
57
+ { required: ['selector'], properties: { targetText: false } }, { required: ['targetText'], properties: { selector: false } }];
58
+ return schema;
59
+ }
60
+ module.exports = { webSchema };
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ const { createHash, randomUUID } = require('node:crypto');
4
+ const { isDeepStrictEqual } = require('node:util');
5
+ const { getHostFactStore } = require('../shared-kernel/host-fact-store');
6
+ const { sanitizePersistentValue } = require('../fact-codec');
7
+ const { CommandError } = require('../command-errors');
8
+
9
+ const partitions = { logs: 'app-log', network: 'network', state: 'state-event', events: 'state-event', dom: 'ui', completion: 'action', 'capture-barrier': 'action' };
10
+ const maxRecordBytes = 128 * 1024;
11
+ const maxPageRecords = 16;
12
+ const digest = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
13
+ const key = (target, stream) => `web:${JSON.stringify([target.sessionId, target.targetId, stream])}`;
14
+
15
+ // Web facts are Host-owned. Every ingress is committed through the existing
16
+ // FactStore; sessions keep connection metadata, never another payload cache.
17
+ class WebSessionStore {
18
+ constructor(getStore = getHostFactStore) { this.getStore = getStore; }
19
+
20
+ record(target, stream, item, { captureId, actionId = null } = {}) {
21
+ if (!partitions[stream]) throw new CommandError('invalid_web_stream', 'Unknown Web capture stream.');
22
+ const payload = { schemaVersion: 'aab.web-fact/v1', stream, ...target, captureId, item };
23
+ if (Buffer.byteLength(JSON.stringify(payload)) > maxRecordBytes)
24
+ throw new CommandError('web_record_too_large', 'Web facts must not exceed 128 KiB.');
25
+ const result = this.getStore().record({ partition: partitions[stream], targetKey: key(target, stream),
26
+ runtimeEpoch: target.runtimeEpoch, actionId, dedupeKey: `${key(target, stream)}:${target.runtimeEpoch}:${captureId}:${digest(payload)}`,
27
+ payload }, { durability: 'sync' });
28
+ const receipt = result.receipt;
29
+ if (result.ok !== true || receipt?.stored !== true)
30
+ throw new CommandError('web_persistence_unavailable', 'The Web fact was not committed; no memory result is substituted.');
31
+ return { stored: true, source: 'host-fact-store', globalSeq: receipt.globalSeq,
32
+ targetKey: key(target, stream), runtimeEpoch: target.runtimeEpoch, captureId,
33
+ representation: 'fact-store-redacted', originalSha256: digest(payload), storedSha256: digest(sanitizePersistentValue(payload)) };
34
+ }
35
+
36
+ read(target, stream, args = {}) {
37
+ const limit = args.limit ?? maxPageRecords;
38
+ if (!partitions[stream] || !Number.isInteger(limit) || limit < 1 || limit > maxPageRecords)
39
+ throw new CommandError('invalid_web_query', 'Web capture queries require a known stream and limit from 1 to 16.');
40
+ const query = { targetKey: key(target, stream), runtimeEpoch: target.runtimeEpoch,
41
+ partitions: [partitions[stream]], limit,
42
+ ...(args.sinceMs === undefined ? {} : { fromObservedAtMs: args.sinceMs }) };
43
+ const store = this.getStore();
44
+ const upper = args.throughCursor === undefined
45
+ ? store.read({ ...query, fromObservedAtMs: Number.MAX_SAFE_INTEGER, limit: 1 }) : null;
46
+ if (upper && upper.ok !== true) return upper;
47
+ const throughCursor = args.throughCursor ?? upper.cursor;
48
+ const page = store.read({ ...query, cursor: args.cursor, throughCursor });
49
+ if (!page.ok) return page;
50
+ const items = page.items.map(fact => {
51
+ const value = fact.payload;
52
+ if (value?.schemaVersion !== 'aab.web-fact/v1' || value.stream !== stream
53
+ || !isDeepStrictEqual({ sessionId: value.sessionId, runtimeEpoch: value.runtimeEpoch, targetId: value.targetId }, target))
54
+ throw new CommandError('invalid_web_fact', 'Stored Web fact does not match the requested document.');
55
+ return { ...value.item, id: fact.globalSeq, captureId: value.captureId, actionId: fact.actionId,
56
+ sessionId: target.sessionId, runtimeEpoch: target.runtimeEpoch, targetId: target.targetId,
57
+ receivedAtMs: fact.timestamps.observedAtMs,
58
+ ref: { source: 'host-fact-store', stream, globalSeq: fact.globalSeq, targetKey: fact.targetKey, runtimeEpoch: fact.runtimeEpoch } };
59
+ });
60
+ // State is a stream of recorded updates. Values describes this returned
61
+ // page only; it is not a fabricated snapshot of every current App key.
62
+ return { ok: true, type: stream, ...target, source: 'host-fact-store', persistence: true,
63
+ history: args.history === true, items, count: items.length, ...(stream === 'state' ? { values: stateValues(items) } : {}),
64
+ cursor: page.cursor, throughCursor, hasMore: page.hasMore, gap: page.gap, cursorExpired: page.cursorExpired };
65
+ }
66
+
67
+ saveCaptureBarrier(target, stream, status) {
68
+ const upper = this.getStore().read({ targetKey: key(target, stream), runtimeEpoch: target.runtimeEpoch,
69
+ partitions: [partitions[stream]], fromObservedAtMs: Number.MAX_SAFE_INTEGER, limit: 1 });
70
+ if (upper.ok !== true) throw new CommandError(upper.error, 'The capture watermark could not be read.');
71
+ const id = randomUUID();
72
+ const value = { stream, cursor: upper.cursor, ...status };
73
+ const receipt = this.record(target, 'capture-barrier', value, { captureId: id, actionId: id });
74
+ return { ...value, token: `wc1:${id}`, globalSeq: receipt.globalSeq };
75
+ }
76
+
77
+ captureBarrier(target, stream, token) {
78
+ if (typeof token !== 'string' || !/^wc1:[a-f0-9-]{36}$/.test(token))
79
+ throw new CommandError('invalid_web_capture_cursor', 'Use a watermark issued for this document and stream.');
80
+ const page = this.getStore().read({ partitions: ['action'], targetKey: key(target, 'capture-barrier'),
81
+ runtimeEpoch: target.runtimeEpoch, actionId: token.slice(4), limit: 2 });
82
+ if (page.ok !== true || page.items.length !== 1)
83
+ throw new CommandError('web_capture_cursor_unavailable', 'The original persisted capture watermark is unavailable.');
84
+ const fact = page.items[0], value = fact.payload;
85
+ if (value?.schemaVersion !== 'aab.web-fact/v1' || value.stream !== 'capture-barrier'
86
+ || value.item.stream !== stream || value.sessionId !== target.sessionId
87
+ || value.runtimeEpoch !== target.runtimeEpoch || value.targetId !== target.targetId)
88
+ throw new CommandError('web_capture_cursor_mismatch', 'The capture watermark belongs to another document or stream.');
89
+ return { ...value.item, token, globalSeq: fact.globalSeq };
90
+ }
91
+
92
+ readWindow(target, stream, args, through) {
93
+ const before = args.factCursor === undefined ? null : this.captureBarrier(target, stream, args.factCursor);
94
+ if (args.afterActionId !== undefined && before === null)
95
+ throw new CommandError('decision_watermark_required', 'Observe a capture watermark before the action.');
96
+ if (before && before.globalSeq > through.globalSeq)
97
+ throw new CommandError('invalid_web_capture_window', 'The upper watermark precedes the lower watermark.');
98
+ const page = this.read(target, stream, { ...args,
99
+ cursor: args.cursor ?? before?.cursor, throughCursor: through.cursor });
100
+ if (page.ok !== true) return page;
101
+ const reasons = [];
102
+ if (before && before.connectionId !== through.connectionId) reasons.push('web_capture_connection_changed');
103
+ const sdkProduced = through.sdk.sequence - (before?.sdk.sequence ?? 0);
104
+ const hostAccepted = through.host.accepted - (before?.host.accepted ?? 0);
105
+ if (sdkProduced !== hostAccepted || through.host.lastSequence !== through.sdk.sequence)
106
+ reasons.push('web_capture_sequence_gap');
107
+ if (through.sdk.losses !== (before?.sdk.losses ?? 0)
108
+ || through.host.rejected !== (before?.host.rejected ?? 0)) reasons.push('web_capture_loss');
109
+ const gap = reasons.length > 0 || page.gap;
110
+ if (through.sdk.pending > 0) reasons.push('web_capture_pending');
111
+ const items = args.afterActionId === undefined ? page.items : page.items.filter(item => item.actionId === args.afterActionId);
112
+ return { ...page, items, count: items.length, ...(stream === 'state' ? { values: stateValues(items) } : {}),
113
+ refs: items.map(item => item.ref), stream,
114
+ targetKey: key(target, stream), runtimeEpoch: target.runtimeEpoch,
115
+ coverage: { status: reasons.length ? 'partial' : 'complete', gap, committed: true,
116
+ scope: 'sdk-captures-through-barrier', reasons }, gap, committed: true,
117
+ window: { filterApplied: true, afterActionId: args.afterActionId ?? null,
118
+ factCursor: args.factCursor ?? null, sinceId: null, sinceMs: args.sinceMs ?? null,
119
+ targetKey: key(target, stream), runtimeEpoch: target.runtimeEpoch },
120
+ watermarkCursor: through.token, throughWatermark: through.globalSeq,
121
+ nextCursor: page.hasMore ? page.cursor : null, throughCursor: through.token,
122
+ barrier: { schemaVersion: 'aab.web-capture-window/v1',
123
+ lower: before === null ? null : { sdk: before.sdk, host: before.host, connectionId: before.connectionId },
124
+ upper: { sdk: through.sdk, host: through.host, connectionId: through.connectionId } },
125
+ };
126
+ }
127
+
128
+ saveCompletion(target, result) {
129
+ return this.record(target, 'completion', { result, originalSha256: digest(result) },
130
+ { captureId: result.actionId, actionId: result.actionId });
131
+ }
132
+
133
+ completion(target, actionId) {
134
+ const page = this.getStore().read({ partitions: ['action'], targetKey: key(target, 'completion'),
135
+ runtimeEpoch: target.runtimeEpoch, actionId, limit: 2 });
136
+ if (!page.ok) return page;
137
+ if (page.items.length !== 1) return { ok: false, error: 'web_original_completion_unavailable', settled: false };
138
+ const fact = page.items[0], payload = fact.payload;
139
+ if (payload?.schemaVersion !== 'aab.web-fact/v1' || payload.stream !== 'completion'
140
+ || !isDeepStrictEqual({ sessionId: payload.sessionId, runtimeEpoch: payload.runtimeEpoch, targetId: payload.targetId }, target)
141
+ || payload.item?.result?.actionId !== actionId)
142
+ return { ok: false, error: 'invalid_web_completion', settled: false };
143
+ return { ok: true, actionId, runtimeEpoch: target.runtimeEpoch, executionResult: payload.item.result,
144
+ receipt: { stored: true, globalSeq: fact.globalSeq, source: 'host-fact-store', originalSha256: payload.item.originalSha256 } };
145
+ }
146
+ }
147
+
148
+ function stateValues(items) {
149
+ const values = {};
150
+ for (const item of items) {
151
+ if (!Object.hasOwn(values, item.namespace)) Object.defineProperty(values, item.namespace, { value: {}, enumerable: true });
152
+ Object.defineProperty(values[item.namespace], item.key, { value: item.value, enumerable: true, configurable: true });
153
+ }
154
+ return values;
155
+ }
156
+
157
+ module.exports = { WebSessionStore, maxRecordBytes, maxPageRecords, digest };