@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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { object, integer, text, milliseconds } = require('./argument-schema');
|
|
4
|
+
const { executionTargetSchema } = require('./execution-target');
|
|
5
|
+
const { observationTargetSchema, intentTargetSchema } = require('../intent/intent-observation-target');
|
|
6
|
+
const { selectorSchema: iosNativeSelector } = require('./ios-native-target');
|
|
7
|
+
|
|
8
|
+
const boolean = { type: 'boolean' };
|
|
9
|
+
const number = { type: 'number', minimum: -2147483647, maximum: 2147483647 };
|
|
10
|
+
const revision = integer(1);
|
|
11
|
+
const afterSequence = { ...integer(0), default: 0 };
|
|
12
|
+
const pageLimit = integer(1, 1000);
|
|
13
|
+
const provider = { enum: ['native', 'uia', 'flutter', 'h5'] };
|
|
14
|
+
const packageName = { ...text, pattern: '^[A-Za-z][A-Za-z0-9_]*(?:\\.[A-Za-z][A-Za-z0-9_]*)+$' };
|
|
15
|
+
const jsonObject = { type: 'object', additionalProperties: true, description: 'Application data; nested values must be JSON.' };
|
|
16
|
+
const array = (items, minItems = 0, maxItems = 1000) => ({ type: 'array', items, minItems, maxItems });
|
|
17
|
+
|
|
18
|
+
function variants(tag, branches) {
|
|
19
|
+
const properties = {};
|
|
20
|
+
for (const branch of branches) {
|
|
21
|
+
for (const [name, rule] of Object.entries(branch.properties)) {
|
|
22
|
+
const prior = properties[name];
|
|
23
|
+
if (!prior) properties[name] = rule;
|
|
24
|
+
else if (JSON.stringify(prior) !== JSON.stringify(rule)) properties[name] = { anyOf: [prior, rule] };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
properties[tag] = { type: 'string', enum: [...new Set(branches.flatMap(branch => branch.properties[tag].enum || [branch.properties[tag].const]))] };
|
|
28
|
+
return { ...object(properties, [tag]), anyOf: branches };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function exactlyOne(properties, keys, required = []) {
|
|
32
|
+
return { ...object(properties, required), oneOf: keys.map(key => ({ required: [key] })) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const nodeLabels = { text, resourceName: text, contentDescription: text };
|
|
36
|
+
const nodeIdentity = exactlyOne({ className: text, resourceName: text }, ['className', 'resourceName']);
|
|
37
|
+
const ancestor = exactlyOne({ ...nodeIdentity.properties, parent: nodeIdentity }, ['className', 'resourceName']);
|
|
38
|
+
const within = exactlyOne({ ...nodeLabels, ancestor }, Object.keys(nodeLabels), ['ancestor']);
|
|
39
|
+
const nativeSelector = exactlyOne({ ...nodeLabels, within }, Object.keys(nodeLabels));
|
|
40
|
+
const uiaSelector = require('./uia-target').intentSelectorSchema;
|
|
41
|
+
const flutterSelector = exactlyOne({ text, nodeId: text }, ['text', 'nodeId']);
|
|
42
|
+
|
|
43
|
+
function nativeGestureBranches(make) {
|
|
44
|
+
const selector = nativeSelector;
|
|
45
|
+
return [
|
|
46
|
+
make('longPress', { selector, durationMs: integer(500, 10000) }, ['selector', 'durationMs']),
|
|
47
|
+
make('swipe', { selector, deltaX: number, deltaY: number, durationMs: integer(1, 10000) }, ['selector', 'deltaX', 'deltaY', 'durationMs']),
|
|
48
|
+
make('scroll', { selector, direction: { enum: ['up', 'down'] }, durationMs: { ...integer(1, 10000), default: 400 } }, ['selector', 'direction']),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function nativeGestureSchema() {
|
|
53
|
+
return variants('action', nativeGestureBranches((action, properties, required) => object({ action: { const: action }, ...properties }, ['action', ...required])));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function intentActionSchema(activeProvider, platform) {
|
|
57
|
+
const branches = [];
|
|
58
|
+
for (const name of activeProvider ? [activeProvider] : provider.enum) {
|
|
59
|
+
const make = (action, properties = {}, required = []) => object({ action: { const: action }, provider: { const: name }, ...properties }, ['action', ...required]);
|
|
60
|
+
if (name === 'h5') {
|
|
61
|
+
const webSelector = require('./web-dom-target').intentSelectorSchema;
|
|
62
|
+
if (platform === 'web' || platform === undefined) {
|
|
63
|
+
branches.push(make('tap', { selector: webSelector }, ['selector']), make('scroll', { selector: webSelector }, ['selector']),
|
|
64
|
+
make('inputText', { selector: webSelector, value: { type: 'string', maxLength: 16384 } }, ['selector', 'value']),
|
|
65
|
+
make('pressKey', { selector: webSelector, key: { enum: ['Enter', 'Escape'] } }, ['selector', 'key']),
|
|
66
|
+
make('scrollBy', { selector: webSelector, deltaX: number, deltaY: number }, ['selector', 'deltaX', 'deltaY']));
|
|
67
|
+
if (platform === 'web') continue;
|
|
68
|
+
}
|
|
69
|
+
const selector = require('./ios-h5-target').selectorSchema;
|
|
70
|
+
branches.push(make('tap', { selector }, ['selector']), make('scroll', { selector }, ['selector']),
|
|
71
|
+
make('inputText', { selector, value: { type: 'string', maxLength: 16384 } }, ['selector', 'value']));
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const native = platform === 'ios' ? iosNativeSelector : platform === 'android' ? nativeSelector
|
|
75
|
+
: { allOf: [object({ ...nativeSelector.properties, ...iosNativeSelector.properties }), { anyOf: [nativeSelector, iosNativeSelector] }] };
|
|
76
|
+
const selector = name === 'native' ? native : name === 'uia' ? uiaSelector : flutterSelector;
|
|
77
|
+
if (name === 'native' && (platform === 'ios' || platform === undefined)) {
|
|
78
|
+
branches.push(make('setOrientation', { orientation: require('./ios-native-target').orientationSchema }, ['orientation']));
|
|
79
|
+
}
|
|
80
|
+
branches.push(make('tap', { selector }, ['selector']));
|
|
81
|
+
if (name === 'native' && platform === 'ios') {
|
|
82
|
+
branches.push(make('inputText', { selector, value: { type: 'string', maxLength: 16384 } }, ['selector', 'value']));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
branches.push(make('back'));
|
|
86
|
+
if (platform !== 'ios') branches.push(make('keyevent', { keyCode: integer(0, 2147483647) }, ['keyCode']));
|
|
87
|
+
if (name === 'native') {
|
|
88
|
+
branches.push(make('inputText', { selector, value: { type: 'string' } }, ['selector', 'value']));
|
|
89
|
+
branches.push(...nativeGestureBranches(make));
|
|
90
|
+
}
|
|
91
|
+
if (name === 'flutter') {
|
|
92
|
+
branches.push(make('inputText', { selector, value: { type: 'string' } }, ['selector', 'value']));
|
|
93
|
+
branches.push(make('scrollBy', { selector, delta: number }, ['selector', 'delta']));
|
|
94
|
+
branches.push(make('hideKeyboard'));
|
|
95
|
+
}
|
|
96
|
+
else if (name === 'uia') branches.push(make('scroll', { direction: { enum: ['up', 'down'] }, durationMs: { ...integer(1, 10000), default: 400 } }, ['direction']));
|
|
97
|
+
}
|
|
98
|
+
return variants('action', branches);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function intentDecisionSchema(activeProvider, platform) {
|
|
102
|
+
const common = { decisionId: text, basedOnRevision: revision, reason: text };
|
|
103
|
+
return variants('agentDecision', [
|
|
104
|
+
object({ ...common, agentDecision: { const: 'act' }, action: intentActionSchema(activeProvider, platform) }, ['decisionId', 'basedOnRevision', 'agentDecision', 'action']),
|
|
105
|
+
...['complete', 'fail', 'inconclusive'].map(value => object({ ...common, agentDecision: { const: value } }, ['decisionId', 'basedOnRevision', 'agentDecision'])),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const intentTarget = intentTargetSchema();
|
|
110
|
+
const scriptTarget = executionTargetSchema({ nullable: true });
|
|
111
|
+
const intentActionNames = intentActionSchema().properties.action.enum;
|
|
112
|
+
const intentBudget = object({ maxSteps: { ...integer(1, 10000), default: 30 }, maxAgentCalls: { ...integer(1, 10000), default: 30 },
|
|
113
|
+
maxDurationMs: { ...milliseconds, default: 120000 }, allowlist: { ...array({ enum: intentActionNames }, 0, intentActionNames.length), uniqueItems: true, default: ['tap'] } });
|
|
114
|
+
const captureRequirements = object({ streams: { ...array({ enum: ['logs', 'network', 'state', 'events'] }, 1, 4), uniqueItems: true },
|
|
115
|
+
view: { enum: ['decision-window', 'connected-history'], default: 'decision-window' }, sinceMs: integer(0), limit: pageLimit,
|
|
116
|
+
timeoutMs: milliseconds, runtimeEpoch: text,
|
|
117
|
+
afterActionId: { anyOf: [text, { type: 'null' }], description: 'Omitted selects the current action. Explicit null retains all records in the observed window without claiming causal attribution.' },
|
|
118
|
+
factCursor: text }, ['streams']);
|
|
119
|
+
|
|
120
|
+
function scriptSpecSchema(permissionNames) {
|
|
121
|
+
return { ...object({ schemaVersion: { const: 'aab.code-script/v1' }, name: { ...text, default: 'script' },
|
|
122
|
+
language: { enum: ['javascript', 'python'] }, source: text, sourcePath: text, cwd: text,
|
|
123
|
+
entrypoint: { ...text, default: 'main' }, target: scriptTarget, inputs: { ...jsonObject, default: {} },
|
|
124
|
+
permissions: { ...array({ enum: permissionNames }, 0, permissionNames.length), uniqueItems: true,
|
|
125
|
+
default: ['app.read', 'app.interact', 'capture.read'] },
|
|
126
|
+
policy: object({ timeoutMs: { ...milliseconds, default: 600000 },
|
|
127
|
+
restartPolicy: { enum: ['none', 'checkpoint'], default: 'none' },
|
|
128
|
+
maxOutputBytes: { ...integer(1, 64 * 1024 * 1024), default: 1048576 }, maxProgressBytes: { ...integer(1, 64 * 1024 * 1024), default: 1048576 } }),
|
|
129
|
+
}, ['schemaVersion', 'language']), oneOf: [{ required: ['source'] }, { required: ['sourcePath'] }] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function executionCommandSchema(command, permissionNames) {
|
|
133
|
+
const read = { afterSequence, limit: pageLimit };
|
|
134
|
+
const scriptRead = { ...read, eventLimit: pageLimit, includeCatalog: boolean };
|
|
135
|
+
const operation = (name, properties, required = []) => object({ operation: { const: name }, ...properties }, ['operation', ...required]);
|
|
136
|
+
const control = (name, properties = {}) => operation(name, { operationId: text, ...properties }, ['operationId']);
|
|
137
|
+
if (command === 'script') return variants('operation', [
|
|
138
|
+
{ ...operation('start', { script: scriptSpecSchema(permissionNames), operationId: text, recordingDir: text, pythonPath: text }, ['script']),
|
|
139
|
+
allOf: [
|
|
140
|
+
{ if: { required: ['pythonPath'] }, then: { properties: { script: { properties: { language: { const: 'python' } } } } } },
|
|
141
|
+
{ if: { required: ['recordingDir'] }, then: { properties: { script: { properties: { policy: { properties: { restartPolicy: { const: 'none' } } } } } } } },
|
|
142
|
+
] },
|
|
143
|
+
control('result'),
|
|
144
|
+
control('status', scriptRead), control('wait', { ...scriptRead, waitMs: { ...integer(0, 60000), default: 30000 } }),
|
|
145
|
+
...['pause', 'resume', 'cancel'].map(name => control(name, scriptRead)),
|
|
146
|
+
operation('decide', { operationId: text, requestId: text, revision, decision: { contentMediaType: 'application/json', description: 'JSON answer to the current ctx.askAgent request; business data is preserved.' }, ...scriptRead }, ['operationId', 'requestId', 'revision', 'decision']),
|
|
147
|
+
operation('runtime-status', {}),
|
|
148
|
+
]);
|
|
149
|
+
if (command === 'intent') {
|
|
150
|
+
const start = { goal: text, target: intentTarget, provider: { ...provider, default: 'native' }, observationTarget: observationTargetSchema, operationId: text,
|
|
151
|
+
timeoutMs: { ...milliseconds, default: 300000, description: 'Total lifetime including observations, decision waits, paused time and actions; terminal persistence drains afterwards.' },
|
|
152
|
+
require: captureRequirements, recordingDir: text };
|
|
153
|
+
return variants('operation', [
|
|
154
|
+
operation('start', { ...start, mode: { const: 'supervised', default: 'supervised' } }, ['goal', 'target']),
|
|
155
|
+
operation('start', { ...start, mode: { const: 'autonomous' }, agentModule: text, budget: intentBudget }, ['goal', 'target', 'mode', 'agentModule']),
|
|
156
|
+
control('status', read), control('observe', { basedOnRevision: revision, observationTarget: observationTargetSchema,
|
|
157
|
+
provider: { ...provider, description: 'Observe through this provider on the frozen target. Becomes selected only after the new observation and summary are committed; omitted retains the last committed selection.' } }),
|
|
158
|
+
operation('decide', { operationId: text, decision: intentDecisionSchema() }, ['operationId', 'decision']),
|
|
159
|
+
...['pause', 'resume', 'cancel'].map(name => control(name)),
|
|
160
|
+
operation('intervene', { operationId: text, reason: text }, ['operationId', 'reason']),
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
if (command === 'evidence') return variants('operation', [
|
|
164
|
+
operation('export', { namespace: { enum: ['intent', 'script'] }, operationId: text, outputDir: text, includeRecordedPayloads: boolean }, ['namespace', 'operationId', 'outputDir']),
|
|
165
|
+
operation('verify', { archiveDir: text, manifestSha256: { type: 'string', pattern: '^[a-f0-9]{64}$' } }, ['archiveDir', 'manifestSha256']),
|
|
166
|
+
]);
|
|
167
|
+
throw new Error(`No execution contract for ${command}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { variants, exactlyOne, jsonObject, flutterSelector, nativeSelector, scriptSpecSchema, intentDecisionSchema, intentActionSchema, nativeGestureSchema,
|
|
171
|
+
intentBudget, captureRequirements, executionCommandSchema };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFile } = require('node:child_process');
|
|
4
|
+
const http = require('node:http');
|
|
5
|
+
const https = require('node:https');
|
|
6
|
+
const { CommandError } = require('../command-errors');
|
|
7
|
+
const { requestDirectory } = require('./request-context');
|
|
8
|
+
const { runExecution, currentExecution, checkExecution, executionFailure, markExecutionDispatched } = require('./execution-scope');
|
|
9
|
+
|
|
10
|
+
function ioFailure(code, message, scope) {
|
|
11
|
+
return new CommandError(code, message, { dispatched: scope?.dispatched === true, ambiguous: scope?.dispatched === true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Cancellation requests termination but resolves only after the subprocess has
|
|
15
|
+
// closed. Releasing a device lease on a Promise.race would leave a live writer.
|
|
16
|
+
function execFileBounded(file, args, { timeoutMs = 15000, mutation = false, execFileImpl = execFile, ...options } = {}) {
|
|
17
|
+
if (!currentExecution()) return runExecution({ mutation }, () => execFileBounded(file, args, { ...options, timeoutMs, mutation, execFileImpl }));
|
|
18
|
+
checkExecution();
|
|
19
|
+
const scope = currentExecution();
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
let stopped;
|
|
22
|
+
let killTimer;
|
|
23
|
+
let timer;
|
|
24
|
+
let completed = false;
|
|
25
|
+
const abort = () => stop(executionFailure(scope));
|
|
26
|
+
const child = execFileImpl(file, args, { cwd: requestDirectory(), ...options, timeout: 0, killSignal: 'SIGKILL' }, (error, stdout, stderr) => {
|
|
27
|
+
completed = true;
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
clearTimeout(killTimer);
|
|
30
|
+
scope?.signal.removeEventListener('abort', abort);
|
|
31
|
+
if (stopped) error = stopped;
|
|
32
|
+
if (error) {
|
|
33
|
+
error.stdout = stdout;
|
|
34
|
+
error.stderr = stderr;
|
|
35
|
+
error.dispatched ??= scope?.dispatched === true;
|
|
36
|
+
error.ambiguous ??= error.dispatched;
|
|
37
|
+
reject(error);
|
|
38
|
+
} else resolve({ stdout, stderr });
|
|
39
|
+
});
|
|
40
|
+
if (completed) return;
|
|
41
|
+
if (mutation && child.pid) markExecutionDispatched();
|
|
42
|
+
const stop = error => {
|
|
43
|
+
if (stopped) return;
|
|
44
|
+
stopped = error;
|
|
45
|
+
child.kill('SIGTERM');
|
|
46
|
+
killTimer = setTimeout(() => child.kill('SIGKILL'), 250);
|
|
47
|
+
};
|
|
48
|
+
timer = setTimeout(() => stop(ioFailure('provider_timeout', `Subprocess timed out after ${timeoutMs} ms.`, scope)), timeoutMs);
|
|
49
|
+
scope?.signal.addEventListener('abort', abort, { once: true });
|
|
50
|
+
if (scope?.signal.aborted) abort();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function httpRequestBounded(url, { method = 'GET', payload, headers = {}, timeoutMs = 10000, maxBytes = 64 * 1024 * 1024 } = {}) {
|
|
55
|
+
if (!currentExecution()) return runExecution({ mutation: method !== 'GET' }, () => httpRequestBounded(url, { method, payload, headers, timeoutMs, maxBytes }));
|
|
56
|
+
checkExecution();
|
|
57
|
+
const scope = currentExecution();
|
|
58
|
+
const body = payload === undefined ? undefined : JSON.stringify(payload);
|
|
59
|
+
const protocol = new URL(url).protocol;
|
|
60
|
+
if (!['http:', 'https:'].includes(protocol)) throw ioFailure('invalid_argument', 'HTTP transport requires an http or https URL.', scope);
|
|
61
|
+
checkExecution();
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
let failure;
|
|
64
|
+
let data = '';
|
|
65
|
+
let bytes = 0;
|
|
66
|
+
let ended = false;
|
|
67
|
+
const request = (protocol === 'https:' ? https : http).request(url, {
|
|
68
|
+
method,
|
|
69
|
+
headers: { ...headers, ...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(body) }) },
|
|
70
|
+
}, response => {
|
|
71
|
+
response.setEncoding('utf8');
|
|
72
|
+
response.on('data', chunk => {
|
|
73
|
+
bytes += Buffer.byteLength(chunk);
|
|
74
|
+
if (bytes > maxBytes) stop(ioFailure('provider_response_too_large', `HTTP response exceeds ${maxBytes} bytes.`, scope));
|
|
75
|
+
else data += chunk;
|
|
76
|
+
});
|
|
77
|
+
response.on('error', error => stop(error));
|
|
78
|
+
response.on('end', () => {
|
|
79
|
+
ended = true;
|
|
80
|
+
if (response.statusCode < 200 || response.statusCode >= 300) failure = Object.assign(new Error(`HTTP ${response.statusCode}: ${data}`),
|
|
81
|
+
{ statusCode: response.statusCode, responseBody: data });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
const stop = error => { failure ||= error; request.destroy(); };
|
|
85
|
+
const abort = () => stop(executionFailure(scope));
|
|
86
|
+
const timer = setTimeout(() => stop(ioFailure('provider_timeout', `HTTP request timed out after ${timeoutMs} ms.`, scope)), timeoutMs);
|
|
87
|
+
scope?.signal.addEventListener('abort', abort, { once: true });
|
|
88
|
+
request.on('error', error => { failure ||= error; });
|
|
89
|
+
request.on('close', () => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
scope?.signal.removeEventListener('abort', abort);
|
|
92
|
+
if (!ended) failure ||= ioFailure('provider_disconnected', 'HTTP response closed before completion.', scope);
|
|
93
|
+
if (failure) {
|
|
94
|
+
failure.url = url;
|
|
95
|
+
failure.dispatched ??= scope?.dispatched === true;
|
|
96
|
+
failure.ambiguous ??= failure.dispatched;
|
|
97
|
+
reject(failure);
|
|
98
|
+
} else resolve(data);
|
|
99
|
+
});
|
|
100
|
+
try { checkExecution(); } catch (error) { stop(error); return; }
|
|
101
|
+
if (method !== 'GET' && scope?.mutation) markExecutionDispatched();
|
|
102
|
+
request.end(body);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { execFileBounded, httpRequestBounded };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createExecutionLedger({ maxEvents = 256, maxExecutions = 256 } = {}) {
|
|
4
|
+
const executions = new Map();
|
|
5
|
+
const tombstones = new Map();
|
|
6
|
+
|
|
7
|
+
function evictOldest() {
|
|
8
|
+
const oldest = executions.keys().next().value;
|
|
9
|
+
if (oldest === undefined) return;
|
|
10
|
+
const bucket = executions.get(oldest);
|
|
11
|
+
tombstones.set(oldest, {
|
|
12
|
+
lastSequence: bucket.sequence,
|
|
13
|
+
lastFact: bucket.events[bucket.events.length - 1] || null,
|
|
14
|
+
gap: true,
|
|
15
|
+
});
|
|
16
|
+
executions.delete(oldest);
|
|
17
|
+
while (tombstones.size > maxExecutions) {
|
|
18
|
+
tombstones.delete(tombstones.keys().next().value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function record(fact) {
|
|
23
|
+
if (!fact || typeof fact.executionId !== 'string' || fact.executionId.length === 0) {
|
|
24
|
+
throw new TypeError('executionId');
|
|
25
|
+
}
|
|
26
|
+
let bucket = executions.get(fact.executionId);
|
|
27
|
+
if (!bucket) {
|
|
28
|
+
const prior = tombstones.get(fact.executionId);
|
|
29
|
+
while (executions.size >= maxExecutions) {
|
|
30
|
+
evictOldest();
|
|
31
|
+
}
|
|
32
|
+
tombstones.delete(fact.executionId);
|
|
33
|
+
bucket = {
|
|
34
|
+
sequence: prior ? prior.lastSequence : 0,
|
|
35
|
+
events: [],
|
|
36
|
+
latestByKind: new Map(),
|
|
37
|
+
gap: prior ? true : false,
|
|
38
|
+
};
|
|
39
|
+
executions.set(fact.executionId, bucket);
|
|
40
|
+
}
|
|
41
|
+
bucket.sequence = Number.isInteger(fact.sequence) ? fact.sequence : bucket.sequence + 1;
|
|
42
|
+
const stored = {
|
|
43
|
+
schemaVersion: 'aab.execution-fact/v1',
|
|
44
|
+
executionId: fact.executionId,
|
|
45
|
+
sequence: bucket.sequence,
|
|
46
|
+
revision: fact.revision,
|
|
47
|
+
kind: fact.kind,
|
|
48
|
+
target: fact.target,
|
|
49
|
+
timestampMs: fact.timestampMs,
|
|
50
|
+
actionId: fact.actionId,
|
|
51
|
+
parentFactId: fact.parentFactId,
|
|
52
|
+
payloadSummary: fact.payloadSummary,
|
|
53
|
+
evidenceRefs: fact.evidenceRefs,
|
|
54
|
+
timings: fact.timings,
|
|
55
|
+
};
|
|
56
|
+
bucket.events.push(stored);
|
|
57
|
+
while (bucket.events.length > maxEvents) {
|
|
58
|
+
bucket.events.shift();
|
|
59
|
+
bucket.gap = true;
|
|
60
|
+
}
|
|
61
|
+
bucket.latestByKind.set(fact.kind, stored);
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
executionId: fact.executionId,
|
|
65
|
+
sequence: bucket.sequence,
|
|
66
|
+
kind: fact.kind,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function snapshot(executionId) {
|
|
71
|
+
const bucket = executions.get(executionId);
|
|
72
|
+
if (bucket) {
|
|
73
|
+
return {
|
|
74
|
+
executionId,
|
|
75
|
+
lastSequence: bucket.sequence,
|
|
76
|
+
lastFact: bucket.events[bucket.events.length - 1],
|
|
77
|
+
gap: bucket.gap === true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const tomb = tombstones.get(executionId);
|
|
81
|
+
if (tomb) {
|
|
82
|
+
return {
|
|
83
|
+
executionId,
|
|
84
|
+
lastSequence: tomb.lastSequence,
|
|
85
|
+
lastFact: tomb.lastFact,
|
|
86
|
+
gap: true,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return { executionId, lastSequence: 0, lastFact: null };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function query(executionId, afterSequence, limit) {
|
|
93
|
+
const bucket = executions.get(executionId);
|
|
94
|
+
if (!bucket) {
|
|
95
|
+
const tomb = tombstones.get(executionId);
|
|
96
|
+
if (tomb) {
|
|
97
|
+
return { items: [], lastSequence: tomb.lastSequence, hasMore: false, gap: true };
|
|
98
|
+
}
|
|
99
|
+
return { items: [], lastSequence: 0, hasMore: false, gap: false };
|
|
100
|
+
}
|
|
101
|
+
const items = bucket.events.filter((event) => event.sequence > afterSequence);
|
|
102
|
+
const limited = items.length > limit ? items.slice(0, limit) : items;
|
|
103
|
+
return {
|
|
104
|
+
items: limited,
|
|
105
|
+
lastSequence: limited.length > 0
|
|
106
|
+
? limited[limited.length - 1].sequence
|
|
107
|
+
: (items.length > 0 ? afterSequence : bucket.sequence),
|
|
108
|
+
hasMore: items.length > limited.length,
|
|
109
|
+
gap: bucket.gap === true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function latest(executionId, kind) {
|
|
114
|
+
const bucket = executions.get(executionId);
|
|
115
|
+
if (!bucket) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const found = bucket.latestByKind.get(kind);
|
|
119
|
+
return found === undefined ? null : found;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { record, snapshot, query, latest };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { createExecutionLedger };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
4
|
+
const { CommandError } = require('../command-errors');
|
|
5
|
+
|
|
6
|
+
const context = new AsyncLocalStorage();
|
|
7
|
+
|
|
8
|
+
// One budget covers queueing, observation, dispatch and provider I/O. Nested
|
|
9
|
+
// calls may shorten it, never restart or extend it. Signals stay inside Host.
|
|
10
|
+
async function runExecution({ timeoutMs, deadlineMs = Infinity, signal, mutation } = {}, action) {
|
|
11
|
+
if (timeoutMs !== undefined && (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647)) {
|
|
12
|
+
throw new CommandError('invalid_argument', 'timeoutMs must be a positive integer in milliseconds.', { field: 'timeoutMs' });
|
|
13
|
+
}
|
|
14
|
+
const parent = context.getStore();
|
|
15
|
+
if (parent?.closed) throw new CommandError('runtime_stopped', 'The owning execution has already settled.');
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const scope = {
|
|
18
|
+
parent, controller, signal: controller.signal, closed: false, dispatched: false,
|
|
19
|
+
mutation: mutation ?? parent?.mutation ?? false,
|
|
20
|
+
deadlineMs: Math.min(parent?.deadlineMs ?? Infinity, deadlineMs, timeoutMs === undefined ? Infinity : Date.now() + timeoutMs),
|
|
21
|
+
};
|
|
22
|
+
const subscriptions = [];
|
|
23
|
+
for (const upstream of new Set([parent?.signal, signal].filter(Boolean))) {
|
|
24
|
+
const abort = () => controller.abort(upstream.reason);
|
|
25
|
+
if (upstream.aborted) abort();
|
|
26
|
+
else {
|
|
27
|
+
upstream.addEventListener('abort', abort, { once: true });
|
|
28
|
+
subscriptions.push(() => upstream.removeEventListener('abort', abort));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const remaining = scope.deadlineMs - Date.now();
|
|
32
|
+
let timer;
|
|
33
|
+
if (remaining <= 0) controller.abort({ code: 'deadline_exceeded' });
|
|
34
|
+
else if (Number.isFinite(remaining)) timer = setTimeout(() => controller.abort({ code: 'deadline_exceeded' }), remaining);
|
|
35
|
+
try {
|
|
36
|
+
return await context.run(scope, () => { checkExecution(); return action(); });
|
|
37
|
+
} finally {
|
|
38
|
+
scope.closed = true;
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
for (const unsubscribe of subscriptions) unsubscribe();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function currentExecution() { return context.getStore(); }
|
|
45
|
+
|
|
46
|
+
// Long-lived collectors own their own lifecycle; starting one from a command
|
|
47
|
+
// must not attach all of its later observations to that command's deadline.
|
|
48
|
+
function withoutExecution(action) { return context.exit(action); }
|
|
49
|
+
|
|
50
|
+
function executionFailure(scope = currentExecution()) {
|
|
51
|
+
const code = scope?.signal.reason?.code || 'runtime_stopped';
|
|
52
|
+
return new CommandError(code, code === 'deadline_exceeded'
|
|
53
|
+
? 'The execution deadline expired.' : 'The execution was stopped.', {
|
|
54
|
+
dispatched: scope?.dispatched === true,
|
|
55
|
+
ambiguous: scope?.dispatched === true,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function checkExecution() {
|
|
60
|
+
const scope = currentExecution();
|
|
61
|
+
if (!scope) return;
|
|
62
|
+
// A busy event loop may not have delivered the timer yet.
|
|
63
|
+
if (Date.now() >= scope.deadlineMs) scope.controller.abort({ code: 'deadline_exceeded' });
|
|
64
|
+
if (scope.closed || scope.signal.aborted) throw executionFailure(scope);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function markExecutionDispatched() {
|
|
68
|
+
for (let scope = currentExecution(); scope; scope = scope.parent) scope.dispatched = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function executionSleep(ms) {
|
|
72
|
+
checkExecution();
|
|
73
|
+
const scope = currentExecution();
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
75
|
+
const finish = (error) => {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
scope?.signal.removeEventListener('abort', abort);
|
|
78
|
+
error ? reject(error) : resolve();
|
|
79
|
+
};
|
|
80
|
+
const abort = () => finish(executionFailure(scope));
|
|
81
|
+
const timer = setTimeout(() => finish(), ms);
|
|
82
|
+
scope?.signal.addEventListener('abort', abort, { once: true });
|
|
83
|
+
});
|
|
84
|
+
checkExecution();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = { runExecution, currentExecution, withoutExecution, checkExecution, markExecutionDispatched, executionFailure, executionSleep };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { object, integer, text, validateValue } = require('./argument-schema');
|
|
4
|
+
const { CommandError } = require('../command-errors');
|
|
5
|
+
const { checksumOf } = require('./evidence-schema');
|
|
6
|
+
const { resolveExecutablePaths } = require('./request-context');
|
|
7
|
+
|
|
8
|
+
const definitions = Object.freeze({
|
|
9
|
+
android: { identity: ['serial', 'packageName'], connection: ['adb', 'port'] },
|
|
10
|
+
ios: { identity: ['deviceId', 'bundleId'], connection: ['runtimeUrl', 'iosHost', 'iosPort', 'wdaUrl', 'wdaRunnerBundleId', 'wdaSessionId', 'webViewId', 'devicectl', 'xcodebuild'] },
|
|
11
|
+
web: { identity: ['sessionId', 'runtimeEpoch'], connection: ['targetId'] },
|
|
12
|
+
});
|
|
13
|
+
const identityText = { ...text, maxLength: 1024, pattern: '^\\S+$' };
|
|
14
|
+
const identifier = { ...text, maxLength: 255, pattern: '^[A-Za-z][A-Za-z0-9_]*(?:\\.[A-Za-z0-9_]+)*$' };
|
|
15
|
+
const allFields = new Set(Object.values(definitions).flatMap(value => [...value.identity, ...value.connection]));
|
|
16
|
+
|
|
17
|
+
function executionTargetSchema({ intent = false, nullable = false } = {}) {
|
|
18
|
+
const branches = Object.entries(definitions).map(([platform, definition]) => {
|
|
19
|
+
const properties = { platform: { const: platform } };
|
|
20
|
+
for (const key of definition.identity) properties[key] = identityText;
|
|
21
|
+
for (const key of definition.connection) properties[key] = text;
|
|
22
|
+
if (platform === 'android') {
|
|
23
|
+
properties.packageName = identifier;
|
|
24
|
+
properties.port = integer(1, 65535);
|
|
25
|
+
if (intent) properties.foregroundPackages = { type: 'array', items: identifier, maxItems: 32, uniqueItems: true };
|
|
26
|
+
}
|
|
27
|
+
if (platform === 'ios') properties.iosPort = integer(1, 65535);
|
|
28
|
+
return object(properties, ['platform', ...definition.identity]);
|
|
29
|
+
});
|
|
30
|
+
return { anyOf: nullable ? [...branches, { type: 'null' }] : branches,
|
|
31
|
+
description: 'Explicit platform and target identity. Connection options belong only to that platform.' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeExecutionTarget(value, { intent = false, nullable = false } = {}) {
|
|
35
|
+
validateValue(value, executionTargetSchema({ intent, nullable }), 'target');
|
|
36
|
+
if (value === null) return null;
|
|
37
|
+
if (value.platform === 'ios') {
|
|
38
|
+
if (value.runtimeUrl !== undefined && (value.iosHost !== undefined || value.iosPort !== undefined)) {
|
|
39
|
+
throw new CommandError('target_connection_conflict', 'runtimeUrl and iosHost/iosPort describe alternative runtime connections.', { field: 'target.runtimeUrl' });
|
|
40
|
+
}
|
|
41
|
+
for (const key of ['runtimeUrl', 'wdaUrl']) {
|
|
42
|
+
if (value[key] === undefined) continue;
|
|
43
|
+
let url;
|
|
44
|
+
try { url = new URL(value[key]); } catch { /* rejected below */ }
|
|
45
|
+
if (!url || !['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash || url.search) {
|
|
46
|
+
throw new CommandError('invalid_target_url', `${key} must be an HTTP(S) base URL without credentials, a query or fragment.`, { field: `target.${key}` });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const target = resolveExecutablePaths(structuredClone(value));
|
|
51
|
+
if (target.foregroundPackages) Object.freeze(target.foregroundPackages);
|
|
52
|
+
return Object.freeze(target);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function targetIdentity(target) {
|
|
56
|
+
if (!target) return null;
|
|
57
|
+
const definition = definitions[target.platform];
|
|
58
|
+
if (!definition) throw new CommandError('invalid_target_platform', 'Target has no supported platform discriminator.', { field: 'target.platform' });
|
|
59
|
+
// Web DOM targets share an SDK session but have distinct observation identity.
|
|
60
|
+
const fields = target.platform === 'web' ? ['sessionId', 'runtimeEpoch', 'targetId'] : definition.identity;
|
|
61
|
+
return `${target.platform}:${JSON.stringify(fields.map(key => target[key] ?? null))}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function targetFingerprint(target) {
|
|
65
|
+
return target === null ? null : checksumOf(target);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function commandPlatform(command) {
|
|
69
|
+
return command.startsWith('ios-') ? 'ios' : command.startsWith('web-') ? 'web' : 'android';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A Script target supplies explicit defaults. A complete per-call identity can
|
|
73
|
+
// select another platform; a partial identity can override defaults only on the
|
|
74
|
+
// same platform. Never mix an Android serial with an iOS/Web connection.
|
|
75
|
+
function bindCommandTarget(command, args, defaultTarget = null, explicitTarget) {
|
|
76
|
+
const platform = commandPlatform(command);
|
|
77
|
+
const definition = definitions[platform];
|
|
78
|
+
const selected = explicitTarget === undefined ? defaultTarget : normalizeExecutionTarget(explicitTarget);
|
|
79
|
+
const fullIdentity = definition.identity.every(key => args[key] !== undefined);
|
|
80
|
+
if (selected && selected.platform !== platform && !fullIdentity) {
|
|
81
|
+
throw new CommandError('target_platform_mismatch', `${command} requires a complete ${platform} target.`, { field: 'target.platform' });
|
|
82
|
+
}
|
|
83
|
+
if (explicitTarget !== undefined && selected.platform !== platform) {
|
|
84
|
+
throw new CommandError('target_platform_mismatch', `${command} does not execute on ${selected.platform}.`, { field: 'options.target.platform' });
|
|
85
|
+
}
|
|
86
|
+
const target = { ...(selected?.platform === platform ? selected : {}), platform };
|
|
87
|
+
delete target.foregroundPackages;
|
|
88
|
+
const schema = require('../command-registry').commandSchema(command);
|
|
89
|
+
for (const key of allFields) {
|
|
90
|
+
if (args[key] === undefined) continue;
|
|
91
|
+
if (!definition.identity.includes(key) && !definition.connection.includes(key)) {
|
|
92
|
+
// For example, Android CDP's targetId selects a WebView page. It is a
|
|
93
|
+
// command argument, not the identity of a desktop Web execution target.
|
|
94
|
+
if (schema.properties?.[key] !== undefined) continue;
|
|
95
|
+
throw new CommandError('target_platform_mismatch', `${key} does not belong to a ${platform} target.`, { field: key });
|
|
96
|
+
}
|
|
97
|
+
if (explicitTarget !== undefined && target[key] !== undefined && target[key] !== args[key]) {
|
|
98
|
+
throw new CommandError('target_argument_conflict', `Argument ${key} conflicts with options.target.`, { field: key });
|
|
99
|
+
}
|
|
100
|
+
target[key] = args[key];
|
|
101
|
+
}
|
|
102
|
+
const normalized = normalizeExecutionTarget(target);
|
|
103
|
+
const bound = { ...args };
|
|
104
|
+
const operationBranches = [...(schema.oneOf || []), ...(schema.anyOf || [])]
|
|
105
|
+
.filter(branch => branch.properties?.operation?.const === args.operation && args.operation !== undefined);
|
|
106
|
+
// Transport options may describe both SDK HTTP and WDA on the same iPhone;
|
|
107
|
+
// each command receives only fields its public contract accepts.
|
|
108
|
+
for (const [key, value] of Object.entries(normalized)) {
|
|
109
|
+
if (key !== 'platform' && schema.properties?.[key] !== undefined
|
|
110
|
+
&& !operationBranches.some(branch => branch.properties?.[key] === false)) bound[key] = value;
|
|
111
|
+
}
|
|
112
|
+
return { target: normalized, args: bound };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { executionTargetSchema, normalizeExecutionTarget, targetIdentity, targetFingerprint, commandPlatform, bindCommandTarget };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const managed = require('./managed-sdk-execution');
|
|
4
|
+
const schema = 'aab.flutter-execution/v1';
|
|
5
|
+
|
|
6
|
+
function executeFlutterAction({ tree, ...options }) {
|
|
7
|
+
return managed.executeManagedAction({ ...options, kind: 'flutter', schema, runtime: tree });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = { schema, executeFlutterAction,
|
|
11
|
+
terminalReceipt: (result, identity) => managed.terminalReceipt(schema, result, identity),
|
|
12
|
+
settlementProof: (result, identity) => managed.settlementProof('flutter', schema, result, identity),
|
|
13
|
+
};
|