@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,397 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createLiveCaptureQuery } = require('../shared-kernel/live-capture-query');
|
|
4
|
+
const { summarizeTree } = require('../shared-kernel/summary-transformer');
|
|
5
|
+
const { createDeviceMutationLease } = require('../shared-kernel/device-mutation-lease');
|
|
6
|
+
const { admitExecutionMutation } = require('../shared-kernel/execution-admission');
|
|
7
|
+
const { authorizeCommand } = require('./script-catalog');
|
|
8
|
+
const { isMutationCommand } = require('../command-registry');
|
|
9
|
+
const { normalizeExecutionTarget, bindCommandTarget, targetFingerprint } = require('../shared-kernel/execution-target');
|
|
10
|
+
const { createScriptCapturePort, isCaptureReadCommand } = require('./script-capture-port');
|
|
11
|
+
const { judgeAssertion, issueObservation, createObservationRegistry } = require('./script-assert');
|
|
12
|
+
|
|
13
|
+
function createScriptHostPort({
|
|
14
|
+
runner,
|
|
15
|
+
actions,
|
|
16
|
+
query,
|
|
17
|
+
mutationLease = createDeviceMutationLease(),
|
|
18
|
+
target = null,
|
|
19
|
+
permissions,
|
|
20
|
+
executionId,
|
|
21
|
+
} = {}) {
|
|
22
|
+
if (typeof actions !== 'function') {
|
|
23
|
+
throw new TypeError('actions');
|
|
24
|
+
}
|
|
25
|
+
const defaultTarget = normalizeExecutionTarget(target, { nullable: true });
|
|
26
|
+
const liveQuery = query || (typeof runner === 'function' ? createLiveCaptureQuery({ runner }) : null);
|
|
27
|
+
const capture = liveQuery ? createScriptCapturePort({ query: liveQuery }) : null;
|
|
28
|
+
const observations = createObservationRegistry();
|
|
29
|
+
const watermarks = createObservationRegistry();
|
|
30
|
+
let preActionWatermarks = new Map();
|
|
31
|
+
let mutationTarget = null;
|
|
32
|
+
let afterActionId = null;
|
|
33
|
+
let mutationRevision = 0;
|
|
34
|
+
let pendingMutations = 0;
|
|
35
|
+
let calls = 0;
|
|
36
|
+
let assertions = 0;
|
|
37
|
+
let actionCalls = 0;
|
|
38
|
+
|
|
39
|
+
async function call(command, args = {}, options = {}) {
|
|
40
|
+
calls += 1;
|
|
41
|
+
const callId = `call-${calls}`;
|
|
42
|
+
const gate = authorizeCommand(command, permissions);
|
|
43
|
+
if (!gate.ok) {
|
|
44
|
+
return unavailableEnvelope({ command, error: gate.error, executionId, callId });
|
|
45
|
+
}
|
|
46
|
+
if ((command === 'logcat' && args.clear === true) || (['webview-network', 'webview-console'].includes(command) && args.script !== undefined)) {
|
|
47
|
+
return unavailableEnvelope({ command, error: 'capture_mutation_not_allowed', executionId, callId });
|
|
48
|
+
}
|
|
49
|
+
if (command === 'page-summary') {
|
|
50
|
+
return pageSummaryResult(args, { executionId, callId });
|
|
51
|
+
}
|
|
52
|
+
let binding;
|
|
53
|
+
try { binding = bindCommandTarget(command, args, defaultTarget, options.target); }
|
|
54
|
+
catch (error) {
|
|
55
|
+
return { ...unavailableEnvelope({ command, error: error.code || 'invalid_target', executionId, callId }),
|
|
56
|
+
field: error.field, message: error.message, dispatched: false, ambiguous: false };
|
|
57
|
+
}
|
|
58
|
+
args = binding.args;
|
|
59
|
+
const boundTarget = binding.target;
|
|
60
|
+
const stamp = result => { result.execution.target = structuredClone(boundTarget); return result; };
|
|
61
|
+
const observed = { afterActionId, mutationRevision, pendingMutation: pendingMutations > 0 };
|
|
62
|
+
if (isCaptureReadCommand(command)) {
|
|
63
|
+
const result = await captureResult(command, args, options, { executionId, callId, capture, target: boundTarget });
|
|
64
|
+
const queryTarget = targetFingerprint(boundTarget);
|
|
65
|
+
const boundaryKey = `${command}:${queryTarget}`;
|
|
66
|
+
const metadata = result.evidence.capture;
|
|
67
|
+
const window = result.evidence.window;
|
|
68
|
+
const preAction = preActionWatermarks.get(boundaryKey);
|
|
69
|
+
const matchesQuery = window.filterApplied === true
|
|
70
|
+
&& window.afterActionId === afterActionIdOf(args, options)
|
|
71
|
+
&& (window.factCursor ?? null) === (args.factCursor ?? null)
|
|
72
|
+
&& (window.sinceId ?? null) === (args.sinceId ?? null)
|
|
73
|
+
&& (window.sinceMs ?? null) === (args.sinceMs ?? null)
|
|
74
|
+
&& typeof metadata.runtimeEpoch === 'string' && metadata.runtimeEpoch.length > 0
|
|
75
|
+
&& window.runtimeEpoch === metadata.runtimeEpoch
|
|
76
|
+
&& typeof metadata.targetKey === 'string' && metadata.targetKey.length > 0
|
|
77
|
+
&& window.targetKey === metadata.targetKey
|
|
78
|
+
&& ((boundTarget.packageName ?? boundTarget.bundleId) == null || (boundTarget.packageName ?? boundTarget.bundleId) === metadata.targetKey)
|
|
79
|
+
&& ((options.runtimeEpoch ?? args.runtimeEpoch) == null || (options.runtimeEpoch ?? args.runtimeEpoch) === metadata.runtimeEpoch)
|
|
80
|
+
&& result.evidence.refs.every((ref) => ref.runtimeEpoch === metadata.runtimeEpoch && ref.targetKey === metadata.targetKey);
|
|
81
|
+
// A continuation page alone cannot prove a whole decision window.
|
|
82
|
+
observed.captureValid = Boolean(matchesQuery && metadata.hasMore === false && args.cursor === undefined
|
|
83
|
+
&& (afterActionIdOf(args, options) === null || afterActionIdOf(args, options) === observed.afterActionId)
|
|
84
|
+
&& (observed.mutationRevision === 0 || (
|
|
85
|
+
queryTarget === mutationTarget && preAction
|
|
86
|
+
&& args.factCursor === preAction.cursor
|
|
87
|
+
&& metadata.runtimeEpoch === preAction.runtimeEpoch
|
|
88
|
+
&& metadata.targetKey === preAction.targetKey
|
|
89
|
+
)));
|
|
90
|
+
const coverage = result.evidence.coverage;
|
|
91
|
+
if (matchesQuery && coverage.status === 'complete' && coverage.gap === false && coverage.committed === true
|
|
92
|
+
&& typeof metadata.watermarkCursor === 'string' && metadata.watermarkCursor.length > 0
|
|
93
|
+
&& !observed.pendingMutation && observed.mutationRevision === mutationRevision) {
|
|
94
|
+
watermarks.set(boundaryKey, {
|
|
95
|
+
cursor: metadata.watermarkCursor,
|
|
96
|
+
runtimeEpoch: metadata.runtimeEpoch,
|
|
97
|
+
targetKey: metadata.targetKey,
|
|
98
|
+
mutationRevision,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
issueObservation(observations, result.evidence, observed, { command, payload: result.result });
|
|
102
|
+
return stamp(result);
|
|
103
|
+
}
|
|
104
|
+
if (isMutationCommand(command, args)) {
|
|
105
|
+
return stamp(await mutationResult(command, args, options, {
|
|
106
|
+
actions,
|
|
107
|
+
mutationLease,
|
|
108
|
+
target: boundTarget,
|
|
109
|
+
executionId,
|
|
110
|
+
callId,
|
|
111
|
+
onAction: (id) => {
|
|
112
|
+
actionCalls += 1;
|
|
113
|
+
afterActionId = id;
|
|
114
|
+
mutationTarget = targetFingerprint(boundTarget);
|
|
115
|
+
preActionWatermarks = new Map(watermarks.entries().filter(([, item]) => item.mutationRevision === mutationRevision));
|
|
116
|
+
mutationRevision += 1;
|
|
117
|
+
pendingMutations += 1;
|
|
118
|
+
},
|
|
119
|
+
onSettled: () => { pendingMutations -= 1; },
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
const result = await actionOnce(command, args, options, {
|
|
123
|
+
actions,
|
|
124
|
+
target: boundTarget,
|
|
125
|
+
executionId,
|
|
126
|
+
callId,
|
|
127
|
+
actionId: null,
|
|
128
|
+
onAction: () => { actionCalls += 1; },
|
|
129
|
+
});
|
|
130
|
+
result.evidence.window.afterActionId = observed.afterActionId;
|
|
131
|
+
issueObservation(observations, result.evidence, observed, {
|
|
132
|
+
command,
|
|
133
|
+
payload: result.result,
|
|
134
|
+
tree: result.ok && ['tree', 'uia-tree', 'flutter-tree', 'flutter-nodes', 'h5-dom', 'flutter-h5-dom', 'ios-tree', 'ios-uia-tree', 'ios-flutter-tree', 'ios-flutter-nodes', 'ios-h5-dom', 'web-dom'].includes(command),
|
|
135
|
+
});
|
|
136
|
+
return stamp(result);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function assert(assertion = {}) {
|
|
140
|
+
assertions += 1;
|
|
141
|
+
return judgeAssertion(assertion, observations, { afterActionId, mutationRevision, pendingMutation: pendingMutations > 0 });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
call,
|
|
146
|
+
assert,
|
|
147
|
+
get callCount() { return calls; },
|
|
148
|
+
get assertionCount() { return assertions; },
|
|
149
|
+
get actionCallCount() { return actionCalls; },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function pageSummaryResult(args, ids) {
|
|
154
|
+
const summary = summarizeTree({
|
|
155
|
+
provider: args.provider,
|
|
156
|
+
rawTree: args.rawTree,
|
|
157
|
+
rawTreeId: args.rawTreeId,
|
|
158
|
+
screenshotId: args.screenshotId,
|
|
159
|
+
});
|
|
160
|
+
return envelope({
|
|
161
|
+
ok: summary.ok !== false,
|
|
162
|
+
command: 'page-summary',
|
|
163
|
+
result: summary,
|
|
164
|
+
error: summary.error || null,
|
|
165
|
+
executionId: ids.executionId,
|
|
166
|
+
callId: ids.callId,
|
|
167
|
+
actionId: null,
|
|
168
|
+
coverage: { status: 'complete', gap: false, committed: true },
|
|
169
|
+
refs: Array.isArray(args.refs) ? args.refs : [],
|
|
170
|
+
timings: {},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function captureResult(command, args, options, { executionId, callId, capture, target }) {
|
|
175
|
+
if (!capture) {
|
|
176
|
+
const result = envelope({
|
|
177
|
+
ok: false,
|
|
178
|
+
error: 'capture_unavailable',
|
|
179
|
+
command,
|
|
180
|
+
result: { items: [] },
|
|
181
|
+
executionId,
|
|
182
|
+
callId,
|
|
183
|
+
actionId: null,
|
|
184
|
+
coverage: { status: 'unavailable', gap: true, committed: false },
|
|
185
|
+
refs: [],
|
|
186
|
+
afterActionId: afterActionIdOf(args, options),
|
|
187
|
+
timings: {},
|
|
188
|
+
});
|
|
189
|
+
result.evidence.capture = {};
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
const afterActionId = afterActionIdOf(args, options);
|
|
193
|
+
const window = await capture.query(command, dispatchArgs(args), {
|
|
194
|
+
actionId: afterActionId,
|
|
195
|
+
timeoutMs: timeoutMsOf(options),
|
|
196
|
+
evidenceWindow: options.evidenceWindow,
|
|
197
|
+
runtimeEpoch: options.runtimeEpoch,
|
|
198
|
+
runtimeEpochChanged: options.runtimeEpochChanged,
|
|
199
|
+
disconnected: options.disconnected,
|
|
200
|
+
});
|
|
201
|
+
const result = envelope({
|
|
202
|
+
ok: window.ok !== false && !window.error && window.coverage.status !== 'unavailable',
|
|
203
|
+
error: window.error || window.reason || null,
|
|
204
|
+
command,
|
|
205
|
+
result: { items: window.items },
|
|
206
|
+
executionId,
|
|
207
|
+
callId,
|
|
208
|
+
actionId: null,
|
|
209
|
+
coverage: window.coverage,
|
|
210
|
+
refs: window.refs,
|
|
211
|
+
afterActionId,
|
|
212
|
+
timings: {},
|
|
213
|
+
});
|
|
214
|
+
result.evidence.window = window.window ? structuredClone(window.window) : { afterActionId: null };
|
|
215
|
+
result.evidence.capture = {
|
|
216
|
+
storeGeneration: window.storeGeneration ?? null,
|
|
217
|
+
throughWatermark: window.throughWatermark ?? null,
|
|
218
|
+
runtimeEpoch: window.runtimeEpoch ?? null,
|
|
219
|
+
targetKey: window.targetKey ?? null,
|
|
220
|
+
watermarkCursor: window.watermarkCursor ?? null,
|
|
221
|
+
nextCursor: window.nextCursor ?? null,
|
|
222
|
+
hasMore: window.hasMore ?? null,
|
|
223
|
+
...(window.barrier === undefined ? {} : { barrier: structuredClone(window.barrier) }),
|
|
224
|
+
};
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function mutationResult(command, args, options, ctx) {
|
|
229
|
+
const actionId = options.dispatchActionId || mutationActionId(ctx.executionId, ctx.callId);
|
|
230
|
+
const bound = dispatchArgs(args, actionId);
|
|
231
|
+
try {
|
|
232
|
+
return await admitExecutionMutation(ctx.target, ctx.mutationLease, async () => {
|
|
233
|
+
try { return await actionOnce(command, bound, options, {
|
|
234
|
+
actions: ctx.actions,
|
|
235
|
+
target: ctx.target,
|
|
236
|
+
executionId: ctx.executionId,
|
|
237
|
+
callId: ctx.callId,
|
|
238
|
+
actionId,
|
|
239
|
+
onAction: ctx.onAction,
|
|
240
|
+
}); } finally { ctx.onSettled(); }
|
|
241
|
+
});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return unavailableEnvelope({ command, error: error.code || error.message, executionId: ctx.executionId, callId: ctx.callId });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function actionOnce(command, args, options, { actions, target, executionId, callId, actionId, onAction }) {
|
|
248
|
+
onAction(actionId);
|
|
249
|
+
let raw;
|
|
250
|
+
try {
|
|
251
|
+
raw = await actions(command, dispatchArgs(args, actionId), options);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return envelope({
|
|
254
|
+
ok: false,
|
|
255
|
+
command,
|
|
256
|
+
result: null,
|
|
257
|
+
error: error?.code || error?.message || String(error),
|
|
258
|
+
dispatched: error?.dispatched,
|
|
259
|
+
ambiguous: error?.ambiguous ?? actionId !== null,
|
|
260
|
+
executionId,
|
|
261
|
+
callId,
|
|
262
|
+
actionId,
|
|
263
|
+
coverage: { status: 'unavailable', gap: true, committed: false },
|
|
264
|
+
refs: [],
|
|
265
|
+
timings: {},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
if (raw && raw.ambiguous === true) {
|
|
269
|
+
return envelope({
|
|
270
|
+
ok: false,
|
|
271
|
+
command,
|
|
272
|
+
result: raw,
|
|
273
|
+
error: raw.error || 'ambiguous',
|
|
274
|
+
ambiguous: true,
|
|
275
|
+
dispatched: raw.dispatched,
|
|
276
|
+
executionReceipt: raw.executionReceipt ?? null,
|
|
277
|
+
executionReceipts: raw.executionReceipts,
|
|
278
|
+
executionId,
|
|
279
|
+
callId,
|
|
280
|
+
actionId,
|
|
281
|
+
coverage: { status: 'unavailable', gap: true, committed: false },
|
|
282
|
+
refs: actionRefs(command, raw),
|
|
283
|
+
timings: raw.timings || {},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return envelope({
|
|
287
|
+
ok: raw != null && raw.ok !== false && !raw.error,
|
|
288
|
+
command,
|
|
289
|
+
result: raw && raw.result !== undefined ? raw.result : raw,
|
|
290
|
+
error: raw && raw.error ? raw.error : null,
|
|
291
|
+
dispatched: raw?.dispatched,
|
|
292
|
+
executionReceipt: raw?.executionReceipt ?? null,
|
|
293
|
+
executionReceipts: raw?.executionReceipts,
|
|
294
|
+
executionId,
|
|
295
|
+
callId,
|
|
296
|
+
actionId,
|
|
297
|
+
coverage: raw == null || raw.ok === false || raw.error
|
|
298
|
+
? { status: 'unavailable', gap: true, committed: false }
|
|
299
|
+
: { status: 'complete', gap: false, committed: true },
|
|
300
|
+
refs: actionRefs(command, raw),
|
|
301
|
+
timings: raw?.timings || {},
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function actionRefs(command, raw) {
|
|
306
|
+
if (raw?.ok === false) return [];
|
|
307
|
+
if (Array.isArray(raw?.refs)) return raw.refs;
|
|
308
|
+
const screenshotPath = command === 'ios-screenshot' ? raw?.outFile : raw?.path;
|
|
309
|
+
if (
|
|
310
|
+
(command === 'screenshot' || command === 'ios-screenshot')
|
|
311
|
+
&& typeof screenshotPath === 'string'
|
|
312
|
+
&& screenshotPath.length > 0
|
|
313
|
+
) {
|
|
314
|
+
return [{ stream: 'screenshot', screenshotId: screenshotPath,
|
|
315
|
+
...(/^[a-f0-9]{64}$/.test(raw.artifact?.sha256 || '') ? { sha256: raw.artifact.sha256 } : {}) }];
|
|
316
|
+
}
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function envelope({
|
|
321
|
+
ok,
|
|
322
|
+
command,
|
|
323
|
+
result,
|
|
324
|
+
error,
|
|
325
|
+
ambiguous,
|
|
326
|
+
executionId,
|
|
327
|
+
callId,
|
|
328
|
+
actionId,
|
|
329
|
+
dispatched = actionId !== null,
|
|
330
|
+
executionReceipt = null,
|
|
331
|
+
executionReceipts,
|
|
332
|
+
coverage,
|
|
333
|
+
refs,
|
|
334
|
+
afterActionId,
|
|
335
|
+
timings,
|
|
336
|
+
}) {
|
|
337
|
+
return {
|
|
338
|
+
ok,
|
|
339
|
+
command,
|
|
340
|
+
result,
|
|
341
|
+
error: error || null,
|
|
342
|
+
ambiguous: ambiguous === true,
|
|
343
|
+
dispatched,
|
|
344
|
+
executionReceipt,
|
|
345
|
+
...(executionReceipts === undefined ? {} : { executionReceipts }),
|
|
346
|
+
execution: { executionId, callId, actionId },
|
|
347
|
+
evidence: {
|
|
348
|
+
window: { afterActionId: afterActionId === undefined ? actionId : afterActionId, closedAtMs: 0 },
|
|
349
|
+
coverage,
|
|
350
|
+
refs,
|
|
351
|
+
},
|
|
352
|
+
timings,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function unavailableEnvelope({ command, error, executionId, callId }) {
|
|
357
|
+
return envelope({
|
|
358
|
+
ok: false,
|
|
359
|
+
command,
|
|
360
|
+
result: null,
|
|
361
|
+
error,
|
|
362
|
+
executionId,
|
|
363
|
+
callId,
|
|
364
|
+
actionId: null,
|
|
365
|
+
coverage: { status: 'unavailable', gap: true, committed: false },
|
|
366
|
+
refs: [],
|
|
367
|
+
timings: {},
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function mutationActionId(executionId, callId) {
|
|
372
|
+
const suffix = `action-${callId.slice('call-'.length)}`;
|
|
373
|
+
if (executionId == null || executionId === '') return suffix;
|
|
374
|
+
return `${executionId}:${suffix}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function dispatchArgs(args, actionId) {
|
|
378
|
+
return { ...args, ...(actionId == null ? {} : { requestId: actionId }) };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function afterActionIdOf(args, options) {
|
|
382
|
+
if (options.evidenceWindow && options.evidenceWindow.afterActionId != null) {
|
|
383
|
+
return options.evidenceWindow.afterActionId;
|
|
384
|
+
}
|
|
385
|
+
if (options.actionId != null) return options.actionId;
|
|
386
|
+
if (args.afterActionId != null) return args.afterActionId;
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function timeoutMsOf(options) {
|
|
391
|
+
if (options.evidenceWindow && options.evidenceWindow.timeoutMs != null) {
|
|
392
|
+
return options.evidenceWindow.timeoutMs;
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
module.exports = { createScriptHostPort, unavailableEnvelope };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createExecutionLedger } = require('../shared-kernel/execution-ledger');
|
|
4
|
+
|
|
5
|
+
function createScriptLedger({ ledger = createExecutionLedger() } = {}) {
|
|
6
|
+
function append(record, type, extra = {}, timestampMs) {
|
|
7
|
+
if (!record || typeof record.operationId !== 'string' || record.operationId.length === 0) {
|
|
8
|
+
throw new TypeError('operationId');
|
|
9
|
+
}
|
|
10
|
+
return ledger.record({
|
|
11
|
+
executionId: record.operationId,
|
|
12
|
+
revision: extra.revision,
|
|
13
|
+
kind: type,
|
|
14
|
+
target: scriptEventTarget(record, type, extra),
|
|
15
|
+
timestampMs,
|
|
16
|
+
actionId: extra.actionId,
|
|
17
|
+
parentFactId: extra.parentFactId,
|
|
18
|
+
payloadSummary: extra.payloadSummary == null ? scriptPayload(extra) : extra.payloadSummary,
|
|
19
|
+
evidenceRefs: extra.evidenceRefs,
|
|
20
|
+
timings: extra.timings,
|
|
21
|
+
sequence: extra.sequence,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { ledger, append };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Call events carry the target already bound before dispatch. Never reconstruct
|
|
29
|
+
// it from a subset of arguments or borrow the owning Script's connection.
|
|
30
|
+
function scriptEventTarget(record, type, extra = {}) {
|
|
31
|
+
return ['call_started', 'call_completed', 'call_failed', 'action_receipt'].includes(type)
|
|
32
|
+
? extra.target : record.spec?.target;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function scriptPayload(extra) {
|
|
36
|
+
return {
|
|
37
|
+
command: extra.command,
|
|
38
|
+
callId: extra.callId,
|
|
39
|
+
args: extra.args,
|
|
40
|
+
error: extra.error,
|
|
41
|
+
status: extra.status,
|
|
42
|
+
resultRef: extra.resultRef,
|
|
43
|
+
name: extra.name,
|
|
44
|
+
verdict: extra.verdict,
|
|
45
|
+
scope: extra.scope,
|
|
46
|
+
reason: extra.reason,
|
|
47
|
+
predicateSummary: extra.predicateSummary,
|
|
48
|
+
condition: extra.condition,
|
|
49
|
+
requiredEvidence: extra.requiredEvidence,
|
|
50
|
+
requireCoverage: extra.requireCoverage,
|
|
51
|
+
refs: extra.refs,
|
|
52
|
+
coverage: extra.coverage,
|
|
53
|
+
observationId: extra.observationId,
|
|
54
|
+
window: extra.window,
|
|
55
|
+
source: extra.source,
|
|
56
|
+
requestId: extra.requestId,
|
|
57
|
+
decision: extra.decision,
|
|
58
|
+
request: extra.request,
|
|
59
|
+
stage: extra.stage,
|
|
60
|
+
message: extra.message,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { createScriptLedger, scriptEventTarget };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { canonicalJson, checksumOf } = require('../shared-kernel/evidence-schema');
|
|
4
|
+
const { sanitizePersistentValue } = require('../fact-codec');
|
|
5
|
+
const { scriptError } = require('./script-errors');
|
|
6
|
+
|
|
7
|
+
// This is an execution output, separate from the bounded progress stream and
|
|
8
|
+
// from live mobile capture queries. It follows the same write-side redaction.
|
|
9
|
+
function prepareScriptResult(value, maxBytes) {
|
|
10
|
+
const original = JSON.parse(JSON.stringify(value ?? null));
|
|
11
|
+
const result = sanitizePersistentValue(original);
|
|
12
|
+
const bytes = Buffer.byteLength(canonicalJson(result));
|
|
13
|
+
if (Buffer.byteLength(canonicalJson(original)) > maxBytes || bytes > maxBytes) {
|
|
14
|
+
return { ok: false, error: 'output_too_large' };
|
|
15
|
+
}
|
|
16
|
+
const sha256 = checksumOf(result), originalSha256 = checksumOf(original);
|
|
17
|
+
return { ok: true, result, bytes, sha256, originalSha256,
|
|
18
|
+
representation: sha256 === originalSha256 ? 'original-json' : 'redacted-json' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resultReference(receipt, prepared) {
|
|
22
|
+
return { evidenceId: receipt.evidenceId, bytes: prepared.bytes, sha256: prepared.sha256,
|
|
23
|
+
originalSha256: prepared.originalSha256, representation: prepared.representation };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readScriptResult({ store, operationId }) {
|
|
27
|
+
if (!store) return scriptError('result_not_persisted', { operationId });
|
|
28
|
+
try {
|
|
29
|
+
let checkpoint = store.latest(operationId, 'checkpoint');
|
|
30
|
+
if (!checkpoint) return scriptError('unknown_operation', { operationId });
|
|
31
|
+
const checked = store.read(checkpoint.evidenceId);
|
|
32
|
+
if (!checked.ok) return scriptError(checked.error, { operationId });
|
|
33
|
+
checkpoint = checked.record;
|
|
34
|
+
if (checkpoint.status !== 'completed') {
|
|
35
|
+
return scriptError(['cancelled', 'failed'].includes(checkpoint.status) ? 'result_unavailable' : 'result_not_ready',
|
|
36
|
+
{ operationId, status: checkpoint.status, executionError: checkpoint.error ?? null });
|
|
37
|
+
}
|
|
38
|
+
const ref = checkpoint.resultRef;
|
|
39
|
+
if (!ref) return scriptError('result_not_persisted', { operationId });
|
|
40
|
+
const loaded = store.read(ref.evidenceId);
|
|
41
|
+
if (!loaded.ok) return scriptError(loaded.error === 'not_found' ? 'result_not_retained' : loaded.error, { operationId, resultRef: ref });
|
|
42
|
+
const body = loaded.record;
|
|
43
|
+
if (body.kind !== 'result' || body.operationId !== operationId || body.revision !== checkpoint.revision
|
|
44
|
+
|| !Object.hasOwn(body, 'result') || checksumOf(body.result) !== ref.sha256
|
|
45
|
+
|| Buffer.byteLength(canonicalJson(body.result)) !== ref.bytes
|
|
46
|
+
|| body.sha256 !== ref.sha256 || body.bytes !== ref.bytes
|
|
47
|
+
|| body.originalSha256 !== ref.originalSha256 || body.representation !== ref.representation) {
|
|
48
|
+
return scriptError('result_checksum_mismatch', { operationId, resultRef: ref });
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, command: 'script', operationId, status: 'completed',
|
|
51
|
+
result: body.result, resultRef: ref, persisted: true };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return scriptError(error.code || 'result_read_failed', { operationId });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { prepareScriptResult, resultReference, readScriptResult };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const readline = require('node:readline');
|
|
5
|
+
|
|
6
|
+
const artifactPath = process.argv[2];
|
|
7
|
+
const replies = new Map();
|
|
8
|
+
const controlWaiters = [];
|
|
9
|
+
let nextId = 0;
|
|
10
|
+
let control = { status: 'running', pauseReason: null };
|
|
11
|
+
|
|
12
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
13
|
+
|
|
14
|
+
function send(message) {
|
|
15
|
+
fs.writeSync(1, `${JSON.stringify(message)}\n`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeStderr(args) {
|
|
19
|
+
fs.writeSync(2, `${args.map(String).join(' ')}\n`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log = (...args) => writeStderr(args);
|
|
23
|
+
console.info = (...args) => writeStderr(args);
|
|
24
|
+
console.debug = (...args) => writeStderr(args);
|
|
25
|
+
|
|
26
|
+
function waitReply(id) {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
if (replies.size >= 32) {
|
|
29
|
+
send({ type: 'fail', error: 'channel_backlog' });
|
|
30
|
+
process.exit(1);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
replies.set(id, resolve);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
rl.on('line', (line) => {
|
|
38
|
+
if (!line) return;
|
|
39
|
+
let message;
|
|
40
|
+
try {
|
|
41
|
+
message = JSON.parse(line);
|
|
42
|
+
} catch {
|
|
43
|
+
send({ type: 'fail', error: 'malformed_frame' });
|
|
44
|
+
process.exit(1);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (message.type === 'start') {
|
|
48
|
+
applyControl(message.control);
|
|
49
|
+
run(message).catch((error) => {
|
|
50
|
+
send({ type: 'fail', error: error.message || String(error) });
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (message.type === 'control') {
|
|
56
|
+
applyControl(message.control);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (message.id && replies.has(message.id)) {
|
|
60
|
+
applyControl(message.control);
|
|
61
|
+
const resolve = replies.get(message.id);
|
|
62
|
+
replies.delete(message.id);
|
|
63
|
+
resolve(message);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
send({ type: 'ready' });
|
|
68
|
+
|
|
69
|
+
function applyControl(next) {
|
|
70
|
+
if (!next) return;
|
|
71
|
+
control = next;
|
|
72
|
+
while (controlWaiters.length > 0) controlWaiters.shift()();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isHeld(status) {
|
|
76
|
+
return status === 'pause_requested'
|
|
77
|
+
|| status === 'paused_manual'
|
|
78
|
+
|| status === 'paused_live'
|
|
79
|
+
|| status === 'paused_ambiguous';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function holdIfPaused() {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const tick = () => {
|
|
85
|
+
if (!isHeld(control.status)) {
|
|
86
|
+
resolve();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
controlWaiters.push(tick);
|
|
90
|
+
};
|
|
91
|
+
tick();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function run(start) {
|
|
96
|
+
const loaded = require(artifactPath);
|
|
97
|
+
const entry = start.entrypoint || 'main';
|
|
98
|
+
const main = typeof loaded === 'function' && entry === 'main' ? loaded : loaded[entry];
|
|
99
|
+
if (typeof main !== 'function') {
|
|
100
|
+
send({ type: 'fail', error: 'unsupported_entrypoint' });
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const ctx = {
|
|
104
|
+
inputs: start.inputs || {},
|
|
105
|
+
call: async (command, args, options) => {
|
|
106
|
+
await holdIfPaused();
|
|
107
|
+
const id = `c${nextId += 1}`;
|
|
108
|
+
send({ type: 'call', id, command, args: args || {}, options: options || {} });
|
|
109
|
+
const reply = await waitReply(id);
|
|
110
|
+
await holdIfPaused();
|
|
111
|
+
return reply.value;
|
|
112
|
+
},
|
|
113
|
+
assert: async (assertion) => {
|
|
114
|
+
await holdIfPaused();
|
|
115
|
+
const id = `a${nextId += 1}`;
|
|
116
|
+
send({ type: 'assert', id, assertion });
|
|
117
|
+
const reply = await waitReply(id);
|
|
118
|
+
await holdIfPaused();
|
|
119
|
+
return reply.value;
|
|
120
|
+
},
|
|
121
|
+
progress: async (event) => {
|
|
122
|
+
await holdIfPaused();
|
|
123
|
+
send({ type: 'progress', event });
|
|
124
|
+
await holdIfPaused();
|
|
125
|
+
},
|
|
126
|
+
checkpoint: async (name, state) => {
|
|
127
|
+
await holdIfPaused();
|
|
128
|
+
const id = `k${nextId += 1}`;
|
|
129
|
+
send({ type: 'checkpoint', id, name, state });
|
|
130
|
+
const reply = await waitReply(id);
|
|
131
|
+
await holdIfPaused();
|
|
132
|
+
return reply.value;
|
|
133
|
+
},
|
|
134
|
+
askAgent: async (request) => {
|
|
135
|
+
await holdIfPaused();
|
|
136
|
+
const id = `q${nextId += 1}`;
|
|
137
|
+
send({ type: 'ask', id, request });
|
|
138
|
+
const reply = await waitReply(id);
|
|
139
|
+
await holdIfPaused();
|
|
140
|
+
return reply.value;
|
|
141
|
+
},
|
|
142
|
+
controlPoint: () => control,
|
|
143
|
+
resume: () => {
|
|
144
|
+
const state = control || {};
|
|
145
|
+
if (!state.checkpoint) return null;
|
|
146
|
+
return state.checkpoint.state;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
const result = await main(ctx);
|
|
150
|
+
send({ type: 'return', result });
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}
|