@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mobileaidev/ai-app-bridge",
3
- "version": "0.2.15",
3
+ "version": "0.3.0-rc.2",
4
4
  "description": "Desktop CLI and MCP server for AI App Bridge across Android, iOS, Flutter, WebView, and Web targets.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,23 +17,78 @@
17
17
  },
18
18
  "files": [
19
19
  "bin/ai-app-bridge.js",
20
+ "bin/bridge-forward.js",
21
+ "bin/command-registry.js",
22
+ "bin/command-errors.js",
20
23
  "bin/artifact-paths.js",
24
+ "bin/connection-cache.js",
25
+ "bin/fact-codec.js",
26
+ "bin/fact-recorder.js",
27
+ "bin/fact-store.js",
28
+ "bin/feedback-probe.js",
29
+ "bin/android-uia-xml.js",
30
+ "bin/android-permissions.js",
31
+ "bin/mmap-scan-index.js",
32
+ "bin/segment-index.js",
33
+ "bin/segmented-fact-store.js",
21
34
  "bin/mcp-server.js",
35
+ "bin/execution-host.js",
36
+ "bin/device-provider.js",
37
+ "bin/command-discovery.js",
38
+ "bin/command-request.js",
39
+ "bin/execution-runtime.js",
40
+ "bin/runtime-client.js",
41
+ "bin/runtime-directory.js",
42
+ "bin/runtime-protocol.js",
43
+ "bin/command-router.js",
44
+ "bin/script",
45
+ "bin/intent",
46
+ "bin/shared-kernel",
47
+ "runtime/uia",
48
+ "bin/observation-collector.js",
22
49
  "bin/ios-provider.js",
50
+ "bin/ios-device-outcome.js",
51
+ "bin/ios-runtime-binding.js",
52
+ "bin/ios-execution.js",
53
+ "bin/target-execution.js",
23
54
  "bin/web-provider.js",
55
+ "bin/web",
24
56
  "skills/ai-app-bridge-use",
25
- "README.md"
57
+ "README.md",
58
+ "docs/SCRIPT_AUTHORING.md",
59
+ "docs/EVIDENCE_ARCHIVE.md",
60
+ "docs/INTENT_NATIVE_EDITING.md",
61
+ "docs/INTENT_FOREGROUND.md",
62
+ "!bin/script/fake-*.js",
63
+ "!bin/script/p9-*.js",
64
+ "!bin/intent/intent-device-adapter.js",
65
+ "docs/COMMAND_CONTRACT.md",
66
+ "bin/ios-wda-port.js",
67
+ "bin/ios-wda-project.js",
68
+ "bin/ios-wda-execution.js",
69
+ "runtime/ios-wda",
70
+ "LICENSE",
71
+ "NOTICE",
72
+ "docs/RELEASE.md"
26
73
  ],
27
74
  "scripts": {
28
- "check": "node -c bin/ai-app-bridge.js && node -c bin/mcp-server.js && node -c bin/ios-provider.js && node --test",
29
- "test": "node --test"
75
+ "check": "node -c bin/ai-app-bridge.js && node -c bin/mcp-server.js && node -c bin/ios-provider.js && node --import ./test-support/ownership-test-sandbox.js --test",
76
+ "test": "node --import ./test-support/ownership-test-sandbox.js --test",
77
+ "verify:package": "node scripts/validation/verify-package.js"
30
78
  },
31
79
  "license": "Apache-2.0",
32
80
  "dependencies": {
33
- "appium-webdriveragent": "^14.1.1",
81
+ "@mobileaidev/segmented-fact-store-native": "file:../../native/segmented-fact-store",
82
+ "appium-webdriveragent": "14.1.1",
34
83
  "ws": "^8.18.0"
35
84
  },
85
+ "bundleDependencies": [
86
+ "@mobileaidev/segmented-fact-store-native"
87
+ ],
36
88
  "publishConfig": {
37
89
  "access": "public"
90
+ },
91
+ "engines": {
92
+ "node": ">=26.3.0 <27"
38
93
  }
39
94
  }
@@ -0,0 +1,19 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ // Process routing identity is published inside the selected Runner's container.
6
+ // It is not a device UDID or an authentication mechanism.
7
+ @interface AABWDABinding : NSObject
8
+ @property(atomic, copy, readonly) NSDictionary *identity;
9
+ @property(atomic, readonly) BOOL ready;
10
+ + (instancetype)shared;
11
+ - (instancetype)initWithDirectory:(NSURL *)directory bundleId:(NSString *)bundleId
12
+ processId:(NSNumber *)processId runtimeEpoch:(NSString *)runtimeEpoch;
13
+ - (BOOL)publishPort:(NSUInteger)port error:(NSError **)error;
14
+ - (nullable NSString *)runtimeErrorForHeaders:(NSDictionary *)headers;
15
+ - (nullable NSString *)targetErrorForHeaders:(NSDictionary *)headers
16
+ actualTarget:(NSDictionary *)target sessionId:(nullable NSString *)sessionId;
17
+ @end
18
+
19
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,97 @@
1
+ #import "AABWDABinding.h"
2
+
3
+ @interface AABWDABinding ()
4
+ @property(atomic, copy, readwrite) NSDictionary *identity;
5
+ @property(atomic, readwrite) BOOL ready;
6
+ @property(nonatomic, strong) NSURL *directory;
7
+ @end
8
+
9
+ @implementation AABWDABinding
10
+ + (instancetype)shared
11
+ {
12
+ static AABWDABinding *value;
13
+ static dispatch_once_t once;
14
+ dispatch_once(&once, ^{
15
+ NSURL *directory = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
16
+ value = [[self alloc] initWithDirectory:directory bundleId:NSBundle.mainBundle.bundleIdentifier
17
+ processId:@(NSProcessInfo.processInfo.processIdentifier) runtimeEpoch:NSUUID.UUID.UUIDString];
18
+ });
19
+ return value;
20
+ }
21
+
22
+ - (instancetype)initWithDirectory:(NSURL *)directory bundleId:(NSString *)bundleId
23
+ processId:(NSNumber *)processId runtimeEpoch:(NSString *)runtimeEpoch
24
+ {
25
+ self = [super init];
26
+ if (self) {
27
+ _directory = directory;
28
+ _identity = @{ @"schemaVersion": @"aab.ios-wda/v1", @"bundleId": bundleId,
29
+ @"runtimeEpoch": runtimeEpoch, @"processId": processId, @"port": @0 };
30
+ }
31
+ return self;
32
+ }
33
+
34
+ - (BOOL)publishPort:(NSUInteger)port error:(NSError **)error
35
+ {
36
+ self.ready = NO;
37
+ if (port < 1 || port > 65535) {
38
+ if (error) *error = [NSError errorWithDomain:@"AABWDABinding" code:1 userInfo:@{NSLocalizedDescriptionKey:@"Invalid WDA port"}];
39
+ return NO;
40
+ }
41
+ NSMutableDictionary *identity = self.identity.mutableCopy;
42
+ identity[@"port"] = @(port);
43
+ self.identity = identity.copy;
44
+ NSMutableDictionary *descriptor = identity.mutableCopy;
45
+ descriptor[@"ok"] = @YES;
46
+ NSData *data = [NSJSONSerialization dataWithJSONObject:descriptor options:NSJSONWritingSortedKeys error:error];
47
+ if (!data || ![NSFileManager.defaultManager createDirectoryAtURL:self.directory withIntermediateDirectories:YES attributes:nil error:error]) return NO;
48
+ self.ready = [data writeToURL:[self.directory URLByAppendingPathComponent:@"ai_app_bridge_wda.json"] options:NSDataWritingAtomic error:error];
49
+ return self.ready;
50
+ }
51
+
52
+ - (nullable NSDictionary *)normalizedHeaders:(NSDictionary *)headers
53
+ {
54
+ NSMutableDictionary *values = [NSMutableDictionary dictionary];
55
+ for (id key in headers) {
56
+ if (![key isKindOfClass:NSString.class] || ![headers[key] isKindOfClass:NSString.class]) return nil;
57
+ NSString *name = [key lowercaseString];
58
+ if (values[name]) return nil;
59
+ values[name] = headers[key];
60
+ }
61
+ return values.copy;
62
+ }
63
+
64
+ - (nullable NSString *)runtimeErrorForHeaders:(NSDictionary *)headers
65
+ {
66
+ if (!self.ready) return @"ios_wda_descriptor_not_ready";
67
+ NSDictionary *values = [self normalizedHeaders:headers];
68
+ if (!values) return @"invalid_ios_wda_headers";
69
+ NSDictionary *identity = self.identity;
70
+ NSDictionary *expected = @{
71
+ @"x-aab-wda-schema": identity[@"schemaVersion"],
72
+ @"x-aab-wda-bundle-id": identity[@"bundleId"],
73
+ @"x-aab-wda-runtime-epoch": identity[@"runtimeEpoch"],
74
+ @"x-aab-wda-process-id": [identity[@"processId"] stringValue],
75
+ @"x-aab-wda-port": [identity[@"port"] stringValue],
76
+ };
77
+ for (NSString *key in expected) {
78
+ if (![values[key] isEqual:expected[key]]) return @"ios_wda_binding_mismatch";
79
+ }
80
+ return nil;
81
+ }
82
+
83
+ - (nullable NSString *)targetErrorForHeaders:(NSDictionary *)headers
84
+ actualTarget:(NSDictionary *)target sessionId:(nullable NSString *)sessionId
85
+ {
86
+ NSDictionary *values = [self normalizedHeaders:headers];
87
+ if (!values) return @"invalid_ios_wda_headers";
88
+ NSString *bundleId = target[@"bundleId"];
89
+ NSNumber *processId = target[@"processId"];
90
+ if (![bundleId isKindOfClass:NSString.class] || !bundleId.length || ![processId isKindOfClass:NSNumber.class] || processId.longLongValue <= 0)
91
+ return @"ios_wda_foreground_unavailable";
92
+ if (![values[@"x-aab-wda-target-bundle-id"] isEqual:bundleId]
93
+ || ![values[@"x-aab-wda-target-process-id"] isEqual:processId.stringValue]) return @"ios_wda_target_changed";
94
+ if (sessionId && ![values[@"x-aab-wda-session-id"] isEqual:sessionId]) return @"ios_wda_session_changed";
95
+ return nil;
96
+ }
97
+ @end
@@ -0,0 +1,26 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import "AABWDAReceiptStore.h"
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+ @class AABWDATask;
6
+ typedef void (^AABWDAReply)(NSDictionary *);
7
+ typedef void (^AABWDAStart)(AABWDATask *);
8
+
9
+ // The execution state is independent of the WDA main UI queue. A task must get
10
+ // permission immediately before each event, and finish only from original callbacks.
11
+ @interface AABWDATask : NSObject
12
+ @property(nonatomic, copy, readonly) NSDictionary *body;
13
+ @property(nonatomic, copy, readonly) NSDictionary *target;
14
+ - (nullable NSString *)permission;
15
+ - (void)finish:(NSDictionary *)outcome;
16
+ - (void)unresolved:(NSString *)reason;
17
+ @end
18
+
19
+ @interface AABWDAExecution : NSObject
20
+ - (instancetype)initWithStore:(AABWDAReceiptStore *)store epoch:(NSString *)epoch;
21
+ - (void)submit:(NSDictionary *)body target:(NSDictionary *)target start:(AABWDAStart)start reply:(AABWDAReply)reply;
22
+ - (NSDictionary *)cancel:(NSString *)actionId epoch:(NSString *)epoch;
23
+ - (NSDictionary *)status;
24
+ - (NSDictionary *)result:(NSString *)actionId epoch:(NSString *)epoch cursor:(nullable NSString *)cursor;
25
+ @end
26
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,172 @@
1
+ #import "AABWDAExecution.h"
2
+
3
+ static NSString *const AABWDASchema = @"aab.wda-execution/v1";
4
+ static NSDictionary *AABWDAFailure(NSString *code)
5
+ {
6
+ return @{@"ok": @NO, @"error": code, @"dispatched": @NO, @"ambiguous": @NO};
7
+ }
8
+ static BOOL AABWDAText(id value)
9
+ {
10
+ return [value isKindOfClass:NSString.class] && [value length] > 0 && [value lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= 256;
11
+ }
12
+
13
+ @interface AABWDATask ()
14
+ @property(nonatomic, copy, readwrite) NSDictionary *body;
15
+ @property(nonatomic, copy, readwrite) NSDictionary *target;
16
+ @property(nonatomic, weak) AABWDAExecution *owner;
17
+ @property(nonatomic, copy) NSDictionary *identity;
18
+ @property(nonatomic) NSTimeInterval deadline;
19
+ @property(nonatomic) BOOL issued;
20
+ @property(nonatomic, copy, nullable) NSString *stopReason;
21
+ @property(nonatomic, copy, nullable) NSDictionary *candidate;
22
+ @property(nonatomic, copy, nullable) AABWDAReply reply;
23
+ @end
24
+
25
+ @interface AABWDAExecution ()
26
+ - (nullable NSString *)permission:(AABWDATask *)task;
27
+ - (void)finish:(AABWDATask *)task outcome:(NSDictionary *)outcome;
28
+ - (void)stop:(AABWDATask *)task reason:(NSString *)reason;
29
+ @end
30
+ @implementation AABWDATask
31
+ - (NSString *)permission { return self.owner ? [self.owner permission:self] : @"ios_wda_action_not_active"; }
32
+ - (void)finish:(NSDictionary *)outcome { [self.owner finish:self outcome:outcome]; }
33
+ - (void)unresolved:(NSString *)reason { AABWDAExecution *owner = self.owner; @synchronized (owner) { [owner stop:self reason:reason]; } }
34
+ @end
35
+
36
+ @implementation AABWDAExecution {
37
+ AABWDAReceiptStore *_store;
38
+ NSString *_epoch;
39
+ AABWDATask *_active;
40
+ NSString *_lastId;
41
+ }
42
+ - (instancetype)initWithStore:(AABWDAReceiptStore *)store epoch:(NSString *)epoch
43
+ {
44
+ self = [super init];
45
+ if (self) { _store = store; _epoch = epoch.copy; }
46
+ return self;
47
+ }
48
+ - (NSDictionary *)pending:(AABWDATask *)task code:(NSString *)code
49
+ {
50
+ NSMutableDictionary *result = task.identity.mutableCopy;
51
+ [result addEntriesFromDictionary:@{@"ok": @NO, @"error": code, @"settled": @NO, @"dispatched": NSNull.null, @"ambiguous": @YES}];
52
+ return result;
53
+ }
54
+ - (void)reply:(AABWDATask *)task value:(NSDictionary *)value
55
+ {
56
+ AABWDAReply reply = task.reply; task.reply = nil;
57
+ // Response serialization can never block the execution lock or re-enter it.
58
+ if (reply) dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ reply(value); });
59
+ }
60
+ - (void)persist:(AABWDATask *)task
61
+ {
62
+ if (_active != task || !task.candidate) return;
63
+ NSDictionary *receipt = [_store commit:task.candidate];
64
+ if (!receipt) { [self reply:task value:[self pending:task code:@"ios_wda_completion_store_unavailable"]]; return; }
65
+ _lastId = task.identity[@"actionId"]; _active = nil;
66
+ [self reply:task value:task.candidate];
67
+ }
68
+ - (void)stop:(AABWDATask *)task reason:(NSString *)reason
69
+ {
70
+ if (_active != task) return;
71
+ if (task.candidate) { [self persist:task]; return; }
72
+ if (task.stopReason) return;
73
+ task.stopReason = reason;
74
+ if (!task.issued) {
75
+ [self finish:task outcome:AABWDAFailure(task.stopReason)];
76
+ } else {
77
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1500 * NSEC_PER_MSEC), dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
78
+ @synchronized (self) { if (self->_active == task) [self reply:task value:[self pending:task code:task.stopReason]]; }
79
+ });
80
+ }
81
+ }
82
+ - (void)submit:(NSDictionary *)body target:(NSDictionary *)target start:(AABWDAStart)start reply:(AABWDAReply)reply
83
+ {
84
+ @synchronized (self) {
85
+ NSDictionary *execution = body[@"execution"];
86
+ id timeout = [execution isKindOfClass:NSDictionary.class] ? execution[@"timeoutMs"] : nil;
87
+ NSSet *fields = [NSSet setWithArray:@[@"schemaVersion", @"actionId", @"runtimeEpoch", @"timeoutMs"]];
88
+ if (![execution isKindOfClass:NSDictionary.class] || ![[NSSet setWithArray:execution.allKeys] isEqual:fields]
89
+ || ![execution[@"schemaVersion"] isEqual:AABWDASchema] || !AABWDAText(execution[@"actionId"])
90
+ || ![body[@"actionId"] isEqual:execution[@"actionId"]] || ![execution[@"runtimeEpoch"] isEqual:_epoch]
91
+ || ![timeout isKindOfClass:NSNumber.class] || CFGetTypeID((__bridge CFTypeRef)timeout) == CFBooleanGetTypeID()
92
+ || [timeout doubleValue] != [timeout longLongValue] || [timeout longLongValue] < 1 || [timeout longLongValue] > INT32_MAX) {
93
+ reply(AABWDAFailure(@"invalid_ios_wda_execution")); return;
94
+ }
95
+ if (!_store.ready) { reply(AABWDAFailure(@"ios_wda_completion_store_unavailable")); return; }
96
+ if (_active) { reply(AABWDAFailure(@"ios_wda_action_busy")); return; }
97
+ if ([_lastId isEqual:execution[@"actionId"]]) { reply(AABWDAFailure(@"ios_wda_action_id_reused")); return; }
98
+ AABWDATask *task = [AABWDATask new];
99
+ task.owner = self; task.body = body.copy; task.target = target.copy; task.reply = reply;
100
+ task.deadline = NSProcessInfo.processInfo.systemUptime + [timeout doubleValue] / 1000;
101
+ task.identity = @{@"schemaVersion": AABWDASchema, @"actionId": execution[@"actionId"], @"runtimeEpoch": _epoch};
102
+ _active = task;
103
+ __weak AABWDATask *weakTask = task;
104
+ __weak AABWDAExecution *weakSelf = self;
105
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, [timeout longLongValue] * NSEC_PER_MSEC), dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
106
+ AABWDAExecution *owner = weakSelf; AABWDATask *pending = weakTask;
107
+ if (owner && pending) @synchronized (owner) { [owner stop:pending reason:@"ios_wda_action_timeout"]; }
108
+ });
109
+ dispatch_async(dispatch_get_main_queue(), ^{
110
+ @synchronized (self) { if (self->_active != task || task.candidate || task.stopReason) return; }
111
+ start(task);
112
+ });
113
+ }
114
+ }
115
+ - (NSString *)permission:(AABWDATask *)task
116
+ {
117
+ @synchronized (self) {
118
+ if (_active != task || task.candidate) return @"ios_wda_action_not_active";
119
+ if (NSProcessInfo.processInfo.systemUptime >= task.deadline) [self stop:task reason:@"ios_wda_action_timeout"];
120
+ if (_active != task || task.stopReason) return task.stopReason ?: @"ios_wda_action_not_active";
121
+ task.issued = YES;
122
+ return nil;
123
+ }
124
+ }
125
+ - (void)finish:(AABWDATask *)task outcome:(NSDictionary *)outcome
126
+ {
127
+ @synchronized (self) {
128
+ if (_active != task || task.candidate) return;
129
+ if (![outcome[@"ok"] isKindOfClass:NSNumber.class] || CFGetTypeID((__bridge CFTypeRef)outcome[@"ok"]) != CFBooleanGetTypeID()
130
+ || (![outcome[@"ok"] boolValue] && !AABWDAText(outcome[@"error"]))) return;
131
+ NSMutableDictionary *result = outcome.mutableCopy;
132
+ if (task.stopReason) { result[@"ok"] = @NO; result[@"error"] = task.stopReason; }
133
+ result[@"actionId"] = task.identity[@"actionId"]; result[@"runtimeEpoch"] = _epoch;
134
+ result[@"settled"] = @YES; result[@"dispatched"] = @(task.issued); result[@"ambiguous"] = @NO;
135
+ NSMutableDictionary *execution = task.identity.mutableCopy;
136
+ execution[@"settled"] = @YES; execution[@"target"] = task.target;
137
+ result[@"execution"] = execution;
138
+ NSData *bytes = [NSJSONSerialization isValidJSONObject:result]
139
+ ? [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingSortedKeys error:NULL] : nil;
140
+ if (!bytes || bytes.length > 64 * 1024) {
141
+ result = [@{@"ok": @NO, @"error": @"ios_wda_invalid_result", @"execution": execution,
142
+ @"actionId": task.identity[@"actionId"], @"runtimeEpoch": _epoch, @"settled": @YES,
143
+ @"dispatched": @(task.issued), @"ambiguous": @NO} mutableCopy];
144
+ }
145
+ task.candidate = result.copy;
146
+ [self persist:task];
147
+ }
148
+ }
149
+ - (NSDictionary *)cancel:(NSString *)actionId epoch:(NSString *)epoch
150
+ {
151
+ @synchronized (self) {
152
+ AABWDATask *task = _active;
153
+ if (task && [task.identity[@"actionId"] isEqual:actionId] && [task.identity[@"runtimeEpoch"] isEqual:epoch]) {
154
+ [self stop:task reason:@"ios_wda_action_cancelled"];
155
+ if (_active == task) return [self pending:task code:@"ios_wda_action_cancel_pending"];
156
+ }
157
+ return [_store lookupAction:actionId epoch:epoch cursor:nil];
158
+ }
159
+ }
160
+ - (NSDictionary *)result:(NSString *)actionId epoch:(NSString *)epoch cursor:(NSString *)cursor
161
+ {
162
+ return [_store lookupAction:actionId epoch:epoch cursor:cursor];
163
+ }
164
+ - (NSDictionary *)status
165
+ {
166
+ @synchronized (self) {
167
+ return @{@"ok": @YES, @"executionSchema": AABWDASchema, @"ready": @(_store.ready),
168
+ @"active": _active ? @{@"identity": _active.identity, @"target": _active.target, @"permissionIssued": @(_active.issued),
169
+ @"persistencePending": @(_active.candidate != nil), @"stopReason": _active.stopReason ?: NSNull.null} : NSNull.null};
170
+ }
171
+ }
172
+ @end
@@ -0,0 +1,71 @@
1
+ // Included once by the prepared FBWebServer translation unit. The upstream
2
+ // package remains untouched; prepare-ios-wda records every source transformation.
3
+ #import "AABWDABinding.m"
4
+ #import "FBCommandHandler.h"
5
+ #import "FBConfiguration.h"
6
+ #import "FBResponseJSONPayload.h"
7
+ #import "FBRouteRequest.h"
8
+ #import "FBSession.h"
9
+ #import "XCUIApplication+FBHelpers.h"
10
+ #import "XCUIApplication+FBQuiescence.h"
11
+ #import "XCUIApplication.h"
12
+
13
+ static NSDictionary *AABWDASessionTarget;
14
+
15
+ static NSDictionary *AABWDAForeground(void)
16
+ {
17
+ XCUIApplication *app = XCUIApplication.fb_activeApplication;
18
+ return app.bundleID && app.processID > 0
19
+ ? @{ @"bundleId": app.bundleID, @"processId": @(app.processID) } : @{};
20
+ }
21
+
22
+ static id<FBResponsePayload> AABWDAReject(NSString *code)
23
+ {
24
+ return [[FBResponseJSONPayload alloc] initWithDictionary:@{
25
+ @"wdaBinding": AABWDABinding.shared.identity, @"sessionId": NSNull.null,
26
+ @"value": @{ @"error": code, @"message": code, @"dispatched": @NO, @"ambiguous": @NO }
27
+ } httpStatusCode:409];
28
+ }
29
+
30
+ // Called by WDA on its main route queue immediately before mounting the route.
31
+ // Session/foreground checks also protect reads: reading a tree cannot launch
32
+ // an App, replace a session, or silently observe another foreground process.
33
+ static NSString *AABWDAAdmission(FBRoute *route, NSDictionary *headers, NSDictionary *parameters, NSData *body)
34
+ {
35
+ NSString *error = [AABWDABinding.shared runtimeErrorForHeaders:headers];
36
+ if (error) return error;
37
+ if (body.length > 4 * 1024 * 1024) return @"ios_wda_request_too_large";
38
+ if (body.length && ![[NSJSONSerialization JSONObjectWithData:body options:0 error:NULL] isKindOfClass:NSDictionary.class])
39
+ return @"invalid_ios_wda_json";
40
+ if ([route.path isEqual:@"/status"] || ([route.path isEqual:@"/aab/session"] && [route.verb isEqual:@"GET"])) return nil;
41
+ if (![route.path hasPrefix:@"/session/:sessionID/"]) return @"ios_wda_route_unsupported";
42
+ FBSession *session = FBSession.activeSession;
43
+ if (!session || !AABWDASessionTarget || ![session.identifier isEqual:AABWDASessionTarget[@"sessionId"]]) return @"ios_wda_session_required";
44
+ if (![parameters[@"sessionID"] isEqual:session.identifier]) return @"ios_wda_session_changed";
45
+ NSDictionary *foreground = AABWDAForeground();
46
+ if (![foreground[@"bundleId"] isEqual:AABWDASessionTarget[@"bundleId"]]
47
+ || ![foreground[@"processId"] isEqual:AABWDASessionTarget[@"processId"]]) return @"ios_wda_target_changed";
48
+ error = [AABWDABinding.shared targetErrorForHeaders:headers actualTarget:foreground sessionId:session.identifier];
49
+ if (error) return error;
50
+ NSSet *allowed = [NSSet setWithArray:@[
51
+ @"GET /session/:sessionID/source", @"POST /session/:sessionID/elements"
52
+ ]];
53
+ return [allowed containsObject:[NSString stringWithFormat:@"%@ %@", route.verb, route.path]] ? nil : @"ios_wda_route_unsupported";
54
+ }
55
+
56
+ @interface AABWDACommands : NSObject <FBCommandHandler>
57
+ @end
58
+
59
+ #import "AABWDAManagedRoutes.h"
60
+ @implementation AABWDACommands
61
+ + (NSArray *)routes
62
+ {
63
+ return @[
64
+ [[FBRoute GET:@"/aab/session"].withoutSession respondWithTarget:self action:@selector(status:)]
65
+ ];
66
+ }
67
+ + (id<FBResponsePayload>)status:(FBRouteRequest *)request
68
+ {
69
+ return FBResponseWithObject(@{ @"foreground": AABWDAForeground(), @"session": AABWDASessionTarget ?: NSNull.null });
70
+ }
71
+ @end