@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.
- package/LICENSE +201 -0
- package/NOTICE +7 -0
- package/README.md +306 -53
- package/bin/ai-app-bridge.js +51 -4504
- package/bin/android-permissions.js +152 -0
- package/bin/android-uia-xml.js +74 -0
- package/bin/artifact-paths.js +127 -1
- package/bin/bridge-forward.js +56 -0
- package/bin/command-discovery.js +86 -0
- package/bin/command-errors.js +60 -0
- package/bin/command-registry.js +504 -0
- package/bin/command-request.js +14 -0
- package/bin/command-router.js +80 -0
- package/bin/connection-cache.js +119 -0
- package/bin/device-provider.js +2934 -0
- package/bin/execution-host.js +596 -0
- package/bin/execution-runtime.js +101 -0
- package/bin/fact-codec.js +321 -0
- package/bin/fact-recorder.js +691 -0
- package/bin/fact-store.js +226 -0
- package/bin/feedback-probe.js +285 -0
- package/bin/intent/install-intent.js +267 -0
- package/bin/intent/intent-action-executor.js +129 -0
- package/bin/intent/intent-autonomous-adapter.js +46 -0
- package/bin/intent/intent-capture-port.js +64 -0
- package/bin/intent/intent-entry.js +226 -0
- package/bin/intent/intent-errors.js +28 -0
- package/bin/intent/intent-evidence-store.js +121 -0
- package/bin/intent/intent-lifetime.js +62 -0
- package/bin/intent/intent-observation-target.js +33 -0
- package/bin/intent/intent-observer.js +194 -0
- package/bin/intent/intent-production-adapter.js +334 -0
- package/bin/intent/intent-provider.js +27 -0
- package/bin/intent/intent-runtime.js +63 -0
- package/bin/intent/intent-worker.js +432 -0
- package/bin/intent/ios-intent-adapter.js +90 -0
- package/bin/intent/permission-intent.js +239 -0
- package/bin/intent/web-intent-adapter.js +31 -0
- package/bin/ios-device-outcome.js +47 -0
- package/bin/ios-execution.js +108 -0
- package/bin/ios-provider.js +497 -583
- package/bin/ios-runtime-binding.js +54 -0
- package/bin/ios-wda-execution.js +70 -0
- package/bin/ios-wda-port.js +98 -0
- package/bin/ios-wda-project.js +79 -0
- package/bin/mcp-server.js +83 -1040
- package/bin/mmap-scan-index.js +329 -0
- package/bin/observation-collector.js +862 -0
- package/bin/runtime-client.js +154 -0
- package/bin/runtime-directory.js +107 -0
- package/bin/runtime-protocol.js +36 -0
- package/bin/script/bounded-script-registry.js +103 -0
- package/bin/script/node-runtime-adapter.js +233 -0
- package/bin/script/progress-projector.js +63 -0
- package/bin/script/python-runtime-adapter.js +111 -0
- package/bin/script/rolling-summary.js +134 -0
- package/bin/script/script-agent-port.js +40 -0
- package/bin/script/script-assert.js +95 -0
- package/bin/script/script-capture-port.js +76 -0
- package/bin/script/script-catalog.js +85 -0
- package/bin/script/script-durable-restore.js +195 -0
- package/bin/script/script-entry-code.js +26 -0
- package/bin/script/script-entry-route.js +38 -0
- package/bin/script/script-entry.js +3 -0
- package/bin/script/script-errors.js +29 -0
- package/bin/script/script-evidence-store.js +22 -0
- package/bin/script/script-format-removed.js +26 -0
- package/bin/script/script-host-port.js +397 -0
- package/bin/script/script-ledger.js +64 -0
- package/bin/script/script-result.js +57 -0
- package/bin/script/script-sdk.js +152 -0
- package/bin/script/script-sdk.py +153 -0
- package/bin/script/script-session-channel.js +127 -0
- package/bin/script/script-spec.js +86 -0
- package/bin/script/script-supervisor.js +919 -0
- package/bin/script/templates/checkpoint-reentry.js +13 -0
- package/bin/segment-index.js +481 -0
- package/bin/segmented-fact-store.js +1571 -0
- package/bin/shared-kernel/android-h5-target.js +10 -0
- package/bin/shared-kernel/android-install-execution.js +176 -0
- package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
- package/bin/shared-kernel/android-shell-execution.js +195 -0
- package/bin/shared-kernel/argument-schema.js +117 -0
- package/bin/shared-kernel/canonical-path.js +17 -0
- package/bin/shared-kernel/device-acknowledgements.js +53 -0
- package/bin/shared-kernel/device-completion-history.js +52 -0
- package/bin/shared-kernel/device-mutation-lease.js +219 -0
- package/bin/shared-kernel/device-ownership-recovery.js +95 -0
- package/bin/shared-kernel/device-ownership-store.js +94 -0
- package/bin/shared-kernel/evidence-adapters.js +251 -0
- package/bin/shared-kernel/evidence-archive.js +329 -0
- package/bin/shared-kernel/evidence-recording.js +131 -0
- package/bin/shared-kernel/evidence-schema.js +194 -0
- package/bin/shared-kernel/evidence-store.js +190 -0
- package/bin/shared-kernel/execution-admission.js +22 -0
- package/bin/shared-kernel/execution-contracts.js +171 -0
- package/bin/shared-kernel/execution-io.js +106 -0
- package/bin/shared-kernel/execution-ledger.js +125 -0
- package/bin/shared-kernel/execution-scope.js +87 -0
- package/bin/shared-kernel/execution-target.js +115 -0
- package/bin/shared-kernel/flutter-execution.js +13 -0
- package/bin/shared-kernel/flutter-h5-port.js +60 -0
- package/bin/shared-kernel/flutter-h5-target.js +9 -0
- package/bin/shared-kernel/flutter-target.js +75 -0
- package/bin/shared-kernel/h5-execution.js +11 -0
- package/bin/shared-kernel/h5-target.js +31 -0
- package/bin/shared-kernel/host-fact-store.js +49 -0
- package/bin/shared-kernel/ios-h5-target.js +9 -0
- package/bin/shared-kernel/ios-native-target.js +71 -0
- package/bin/shared-kernel/live-capture-query.js +115 -0
- package/bin/shared-kernel/managed-sdk-execution.js +78 -0
- package/bin/shared-kernel/native-execution.js +13 -0
- package/bin/shared-kernel/native-target.js +156 -0
- package/bin/shared-kernel/provider-command-contracts.js +55 -0
- package/bin/shared-kernel/recorded-payload-archive.js +195 -0
- package/bin/shared-kernel/request-context.js +35 -0
- package/bin/shared-kernel/semantic-node.js +55 -0
- package/bin/shared-kernel/summary-transformer.js +352 -0
- package/bin/shared-kernel/target-lease-protocol.js +47 -0
- package/bin/shared-kernel/text-wait.js +111 -0
- package/bin/shared-kernel/uia-execution.js +96 -0
- package/bin/shared-kernel/uia-protocol.js +214 -0
- package/bin/shared-kernel/uia-runtime-port.js +377 -0
- package/bin/shared-kernel/uia-target.js +39 -0
- package/bin/shared-kernel/web-dom-target.js +44 -0
- package/bin/shared-kernel/xml-attributes.js +25 -0
- package/bin/target-execution.js +275 -0
- package/bin/web/command-schema.js +60 -0
- package/bin/web/session-store.js +157 -0
- package/bin/web-provider.js +334 -553
- package/docs/COMMAND_CONTRACT.md +1563 -0
- package/docs/EVIDENCE_ARCHIVE.md +214 -0
- package/docs/INTENT_FOREGROUND.md +71 -0
- package/docs/INTENT_NATIVE_EDITING.md +79 -0
- package/docs/RELEASE.md +59 -0
- package/docs/SCRIPT_AUTHORING.md +489 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
- package/package.json +60 -5
- package/runtime/ios-wda/AABWDABinding.h +19 -0
- package/runtime/ios-wda/AABWDABinding.m +97 -0
- package/runtime/ios-wda/AABWDAExecution.h +26 -0
- package/runtime/ios-wda/AABWDAExecution.m +172 -0
- package/runtime/ios-wda/AABWDAIntegration.h +71 -0
- package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
- package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
- package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
- package/runtime/uia/ai-app-bridge-uia.jar +0 -0
- package/runtime/uia/manifest.json +22 -0
- 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
|
|
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
|
-
"
|
|
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
|