@mobileaidev/ai-app-bridge 0.3.8 → 0.4.0
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/README.md +52 -39
- package/bin/ai-app-bridge.js +56 -17
- package/bin/command-discovery.js +19 -4
- package/bin/command-registry.js +15 -4
- package/bin/command-request.js +68 -4
- package/bin/execution-host.js +31 -5
- package/bin/execution-runtime.js +17 -10
- package/bin/executors/preparation.js +19 -5
- package/bin/extraction/json-value.js +26 -0
- package/bin/extraction/prepare.js +57 -0
- package/bin/extraction/regex.js +30 -0
- package/bin/extraction/runner.js +77 -0
- package/bin/mcp-server.js +15 -20
- package/bin/public-reply.js +184 -0
- package/bin/response-store.js +60 -0
- package/bin/runtime-client.js +32 -15
- package/bin/runtime-directory.js +37 -8
- package/bin/script/node-runtime-adapter.js +139 -123
- package/bin/script/python-runtime-adapter.js +1 -1
- package/bin/script/script-diagnostics.js +21 -0
- package/bin/script/script-durable-restore.js +1 -0
- package/bin/script/script-sdk.js +39 -4
- package/bin/script/script-sdk.py +79 -7
- package/bin/script/script-session-channel.js +27 -10
- package/bin/script/script-supervisor.js +8 -0
- package/bin/shared-kernel/argument-schema.js +44 -12
- package/bin/shared-kernel/evidence-archive.js +2 -2
- package/bin/shared-kernel/evidence-schema.js +16 -1
- package/bin/shared-kernel/evidence-store.js +3 -3
- package/bin/shared-kernel/execution-contracts.js +13 -5
- package/docs/COMMAND_CONTRACT.md +91 -19
- package/docs/EVIDENCE_ARCHIVE.md +14 -1
- package/docs/INSTALLATION.md +73 -0
- package/docs/INTENT_FOREGROUND.md +4 -1
- package/docs/OPTIONAL_EXECUTORS.md +14 -14
- package/docs/RELEASE.md +60 -122
- package/docs/RESPONSE_EXTRACTION.md +122 -0
- package/docs/SCRIPT_AUTHORING.md +125 -6
- package/node_modules/@mobileaidev/segmented-fact-store-native/PREBUILDS.md +29 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding-path.js +29 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +1 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +1 -3
- package/node_modules/@mobileaidev/segmented-fact-store-native/install.js +5 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +11 -5
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/darwin-arm64/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/darwin-x64/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/linux-arm64-glibc/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/linux-x64-glibc/segmented_fact_store.node +0 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/prebuilds/manifest.json +27 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/scripts/build-release-prebuilds.js +33 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/scripts/stage-prebuild.js +17 -0
- package/package.json +12 -5
- package/runtime/executors/android/prepare.init.gradle +22 -0
- package/runtime/executors/playwright/package-lock.json +2 -2
- package/runtime/executors/playwright/package.json +1 -1
- package/skills/ai-app-bridge-use/SKILL.md +19 -4
package/README.md
CHANGED
|
@@ -7,10 +7,11 @@ discovery. Every request checks the mapping before dispatch. Mutating requests
|
|
|
7
7
|
are never replayed after a missing route or uncertain result. For manual cleanup,
|
|
8
8
|
pass the exact serial and returned Host port to `remove-forward`.
|
|
9
9
|
|
|
10
|
-
This source version is `0.
|
|
10
|
+
This source version is `0.4.0`; registry publication is a separate release step.
|
|
11
11
|
The default installation includes the Script/Intent and capture contracts below.
|
|
12
12
|
Local package verification does not change npm dist-tags.
|
|
13
13
|
The supported Node range is `>=26.3.0 <27`; this release was checked on 26.3.0.
|
|
14
|
+
See [the simplest MCP setup and Host requirements](docs/INSTALLATION.md).
|
|
14
15
|
See [the release guide](docs/RELEASE.md) for local packaging and coordinated publication.
|
|
15
16
|
|
|
16
17
|
Optional Android Instrumentation/UI Automator/Espresso/Compose/H5, Flutter integration_test,
|
|
@@ -27,13 +28,17 @@ CLI and MCP connect to one independent local execution runtime. Every registered
|
|
|
27
28
|
command is available from either entrypoint, including Intent, Script, Web,
|
|
28
29
|
installation and permission flows. A CLI command can start a task and a later MCP
|
|
29
30
|
connection can observe or decide the same operation ID. Disconnecting either
|
|
30
|
-
client leaves tasks running. Use `runtime --operation status` to inspect its
|
|
31
|
-
owner, `runtime --operation stop` to drain it, and task `cancel` to stop one task.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
client leaves tasks running. Use `runtime --operation status --extract null` to inspect its
|
|
32
|
+
owner, `runtime --operation stop --extract null` to drain it, and task `cancel` to stop one task.
|
|
33
|
+
|
|
34
|
+
Each run explicitly supplies `extract`: `null` for the original result, or
|
|
35
|
+
regex/JavaScript/Python for a smaller result. CLI: `--extract null`.
|
|
36
|
+
The compact body is `{command, execution, control, extraction, delivery, kind,
|
|
37
|
+
value?, failureStage?}`. Original execution facts and continuation controls stay
|
|
38
|
+
separate from the extracted value. Default body budget: 96 KiB UTF-8; overflow
|
|
39
|
+
can return a saved response ref. Retry with `response read`, not the original
|
|
40
|
+
action. CLI exits 0 / 1 / 2 for delivery success / execution failure or unknown /
|
|
41
|
+
post-success extraction or delivery failure. See [extraction and recovery](docs/RESPONSE_EXTRACTION.md).
|
|
37
42
|
|
|
38
43
|
Execution operations and nested controls use strict schemas. Supply an explicit
|
|
39
44
|
`operation`, use `script.target` and canonical `javascript`/`python` languages,
|
|
@@ -60,31 +65,31 @@ domains, commands, and options, then call `run` with the selected command.
|
|
|
60
65
|
|
|
61
66
|
```bash
|
|
62
67
|
# Install the current stable release; see docs/RELEASE.md for packaging.
|
|
63
|
-
npm install -g @mobileaidev/ai-app-bridge@0.
|
|
64
|
-
|
|
65
|
-
ai-app-bridge status --package-name io.github.mobileaidev.aiappbridge.sample
|
|
66
|
-
ai-app-bridge tree --package-name io.github.mobileaidev.aiappbridge.sample
|
|
67
|
-
ai-app-bridge clear-app-data --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample
|
|
68
|
-
ai-app-bridge launch-app --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample
|
|
69
|
-
ai-app-bridge launch-activity --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample --activity .MainActivity --extra route=/home
|
|
70
|
-
ai-app-bridge screenshot --package-name io.github.mobileaidev.aiappbridge.sample
|
|
71
|
-
ai-app-bridge input-text --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample --text "中文输入" --hide-keyboard
|
|
72
|
-
ai-app-bridge network --package-name io.github.mobileaidev.aiappbridge.sample --compact --url-filter /api/
|
|
73
|
-
ai-app-bridge webview-network --package-name io.github.mobileaidev.aiappbridge.sample --duration-ms 3000
|
|
74
|
-
ai-app-bridge ios-devices
|
|
75
|
-
ai-app-bridge ios-doctor --device-id <device-or-udid> --bundle-id <ios.bundle.id> --wda-runner-bundle-id <runner-from-setup>
|
|
76
|
-
ai-app-bridge ios-setup --device-id <device-or-udid> --bundle-id <ios.bundle.id> --team-id <APPLE_TEAM_ID> --start-wda
|
|
77
|
-
ai-app-bridge ios-status --device-id <device-or-udid> --bundle-id <ios.bundle.id>
|
|
78
|
-
ai-app-bridge ios-execution --operation status --device-id <device-or-udid> --bundle-id <ios.bundle.id>
|
|
79
|
-
ai-app-bridge ios-execution --operation reconcile --device-id <device-or-udid> --bundle-id <original.ios.bundle.id>
|
|
80
|
-
ai-app-bridge ios-execution --operation reconcile --kind wda --device-id <device-or-udid> --wda-runner-bundle-id <runner.bundle.id>
|
|
81
|
-
ai-app-bridge ios-wda-session --operation create --device-id <device-or-udid> --wda-runner-bundle-id <runner-from-setup> --bundle-id <ios.bundle.id>
|
|
82
|
-
ai-app-bridge ios-tap --device-id <device-or-udid> --wda-runner-bundle-id <runner-from-setup> --bundle-id <ios.bundle.id> --wda-session-id <created-session> --tap-x 120 --tap-y 360
|
|
83
|
-
ai-app-bridge ios-input --device-id <device-or-udid> --wda-runner-bundle-id <runner-from-setup> --bundle-id <ios.bundle.id> --wda-session-id <created-session> --accessibility-id sample_text_field --clear-first --text "hello"
|
|
84
|
-
ai-app-bridge web-session-start --web-port 18180
|
|
85
|
-
ai-app-bridge runtime --operation status
|
|
86
|
-
ai-app-bridge thaw-app --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample
|
|
87
|
-
ai-app-bridge freeze-app --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample
|
|
68
|
+
npm install -g @mobileaidev/ai-app-bridge@0.4.0
|
|
69
|
+
|
|
70
|
+
ai-app-bridge status --extract null --package-name io.github.mobileaidev.aiappbridge.sample
|
|
71
|
+
ai-app-bridge tree --extract null --package-name io.github.mobileaidev.aiappbridge.sample
|
|
72
|
+
ai-app-bridge clear-app-data --extract null --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample
|
|
73
|
+
ai-app-bridge launch-app --extract null --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample
|
|
74
|
+
ai-app-bridge launch-activity --extract null --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample --activity .MainActivity --extra route=/home
|
|
75
|
+
ai-app-bridge screenshot --extract null --package-name io.github.mobileaidev.aiappbridge.sample
|
|
76
|
+
ai-app-bridge input-text --extract null --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample --text "中文输入" --hide-keyboard
|
|
77
|
+
ai-app-bridge network --extract null --package-name io.github.mobileaidev.aiappbridge.sample --compact --url-filter /api/
|
|
78
|
+
ai-app-bridge webview-network --extract null --package-name io.github.mobileaidev.aiappbridge.sample --duration-ms 3000
|
|
79
|
+
ai-app-bridge ios-devices --extract null
|
|
80
|
+
ai-app-bridge ios-doctor --extract null --device-id <device-or-udid> --bundle-id <ios.bundle.id> --wda-runner-bundle-id <runner-from-setup>
|
|
81
|
+
ai-app-bridge ios-setup --extract null --device-id <device-or-udid> --bundle-id <ios.bundle.id> --team-id <APPLE_TEAM_ID> --start-wda
|
|
82
|
+
ai-app-bridge ios-status --extract null --device-id <device-or-udid> --bundle-id <ios.bundle.id>
|
|
83
|
+
ai-app-bridge ios-execution --extract null --operation status --device-id <device-or-udid> --bundle-id <ios.bundle.id>
|
|
84
|
+
ai-app-bridge ios-execution --extract null --operation reconcile --device-id <device-or-udid> --bundle-id <original.ios.bundle.id>
|
|
85
|
+
ai-app-bridge ios-execution --extract null --operation reconcile --kind wda --device-id <device-or-udid> --wda-runner-bundle-id <runner.bundle.id>
|
|
86
|
+
ai-app-bridge ios-wda-session --extract null --operation create --device-id <device-or-udid> --wda-runner-bundle-id <runner-from-setup> --bundle-id <ios.bundle.id>
|
|
87
|
+
ai-app-bridge ios-tap --extract null --device-id <device-or-udid> --wda-runner-bundle-id <runner-from-setup> --bundle-id <ios.bundle.id> --wda-session-id <created-session> --tap-x 120 --tap-y 360
|
|
88
|
+
ai-app-bridge ios-input --extract null --device-id <device-or-udid> --wda-runner-bundle-id <runner-from-setup> --bundle-id <ios.bundle.id> --wda-session-id <created-session> --accessibility-id sample_text_field --clear-first --text "hello"
|
|
89
|
+
ai-app-bridge web-session-start --extract null --web-port 18180
|
|
90
|
+
ai-app-bridge runtime --extract null --operation status
|
|
91
|
+
ai-app-bridge thaw-app --extract null --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample
|
|
92
|
+
ai-app-bridge freeze-app --extract null --serial DEVICE --package-name io.github.mobileaidev.aiappbridge.sample
|
|
88
93
|
ai-app-bridge-mcp --help
|
|
89
94
|
```
|
|
90
95
|
|
|
@@ -203,7 +208,13 @@ Script/Intent history to inspect execution records separately:
|
|
|
203
208
|
```json
|
|
204
209
|
{
|
|
205
210
|
"command": "events",
|
|
206
|
-
"
|
|
211
|
+
"extract": null,
|
|
212
|
+
"arguments": {
|
|
213
|
+
"serial": "DEVICE",
|
|
214
|
+
"packageName": "com.example.app",
|
|
215
|
+
"history": true,
|
|
216
|
+
"limit": 100
|
|
217
|
+
}
|
|
207
218
|
}
|
|
208
219
|
```
|
|
209
220
|
|
|
@@ -396,10 +407,12 @@ entry point explicitly.
|
|
|
396
407
|
|
|
397
408
|
`ai-app-bridge --version` (also `-V`) reads the installed entrypoint's package
|
|
398
409
|
version without requiring a device. Upgrading npm replaces files; it does not
|
|
399
|
-
replace an MCP process already connected to Cursor or another client.
|
|
400
|
-
|
|
401
|
-
|
|
410
|
+
replace an MCP process already connected to Cursor or another client. Reconnect
|
|
411
|
+
the client's MCP server after upgrading. Follow the mismatch diagnostic: upgrade
|
|
412
|
+
an older client first; only stop an older Runtime after its existing work finishes,
|
|
413
|
+
using `ai-app-bridge runtime --operation stop --extract null`. The MCP initialize
|
|
402
414
|
response reports `serverInfo.version`; refresh cached tool descriptions in the
|
|
403
415
|
client when they still show removed commands such as `batch` or `smoke`.
|
|
404
|
-
A
|
|
405
|
-
|
|
416
|
+
A same-version fingerprint mismatch requires aligning the source and Node
|
|
417
|
+
environment; the hash alone does not identify which side is newer. In-flight
|
|
418
|
+
work is not silently moved to new code.
|
package/bin/ai-app-bridge.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const { CommandError
|
|
4
|
+
const { CommandError } = require('./command-errors');
|
|
5
5
|
const { commandDefinitions, isolatedCommandDefinitions, parseCliOptions } = require('./command-registry');
|
|
6
|
-
const {
|
|
7
|
-
const {
|
|
6
|
+
const { commandHelpSchema } = require('./command-discovery');
|
|
7
|
+
const { publicFailure, exitCodeFor } = require('./public-reply');
|
|
8
|
+
const { publicOutputLimit } = require('./command-request');
|
|
8
9
|
const runtime = require('./runtime-client');
|
|
9
10
|
|
|
10
11
|
const helpText = `Usage: ai-app-bridge <command> [options]
|
|
@@ -25,14 +26,18 @@ Intent decide also accepts --platform, --provider and --action schema filters.
|
|
|
25
26
|
CLI flags use kebab-case, for example --package-name, --tap-x, --timeout-ms.
|
|
26
27
|
Objects, arrays, nullable objects and Script decisions use JSON flag values.
|
|
27
28
|
Only --category and --extra repeat as individual strings.
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
Every command requires --extract: null delivers the command's own result.
|
|
30
|
+
Results are one line of compact JSON: {command, execution, control, extraction,
|
|
31
|
+
delivery, kind: "json"|"text"|"bytes", value, failureStage?}. Bytes are base64.
|
|
32
|
+
Exit 0: delivered as requested. 1: rejected, failed or unknown execution.
|
|
33
|
+
2: the command succeeded but extraction or delivery failed. Help is plain text.
|
|
30
34
|
Relative file paths use the calling directory. Script cwd defaults to it.
|
|
31
|
-
Example: ai-app-bridge script --operation start --script '{"schemaVersion":"aab.code-script/v1","language":"javascript","sourcePath":"./regression.js"}'
|
|
35
|
+
Example: ai-app-bridge script --extract null --operation start --script '{"schemaVersion":"aab.code-script/v1","language":"javascript","sourcePath":"./regression.js","target":{"platform":"android","serial":"<serial>","packageName":"<package>"}}'
|
|
32
36
|
`;
|
|
33
37
|
|
|
34
38
|
async function main() {
|
|
35
39
|
let command;
|
|
40
|
+
let maxBytes;
|
|
36
41
|
const connection = new AbortController();
|
|
37
42
|
const disconnect = () => connection.abort();
|
|
38
43
|
process.once('SIGINT', disconnect);
|
|
@@ -46,29 +51,61 @@ async function main() {
|
|
|
46
51
|
if (!argv.length) { process.stdout.write(`${helpText}\n`); return; }
|
|
47
52
|
const parsed = parseArgs(argv);
|
|
48
53
|
command = parsed.command;
|
|
54
|
+
const envelopeOutput = publicFields({ output: parsed.options.output });
|
|
55
|
+
maxBytes = publicOutputLimit(envelopeOutput);
|
|
56
|
+
if (parsed.error) throw parsed.error;
|
|
49
57
|
if (parsed.options.help || command === 'help') {
|
|
50
58
|
const name = command === 'help' ? '' : command;
|
|
51
59
|
const { help, ...filters } = parsed.options;
|
|
52
60
|
if (!name && Object.keys(filters).length) throw new CommandError('invalid_argument', 'Schema filters require a command after --help.', { field: Object.keys(filters)[0] });
|
|
53
|
-
|
|
61
|
+
const schema = name ? commandHelpSchema(name, filters) : null;
|
|
62
|
+
process.stdout.write(name ? `${JSON.stringify(schema)}\n` : `${helpText}\n`);
|
|
63
|
+
if (schema?.ok === false) process.exitCode = 1;
|
|
54
64
|
return;
|
|
55
65
|
}
|
|
56
66
|
command ||= 'status';
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
// The public fields leave the CLI options before the command's own parser
|
|
68
|
+
// sees them; each is one JSON value and never reaches the device action.
|
|
69
|
+
const { extract, output, ...options } = parsed.options;
|
|
70
|
+
const request = { command, ...envelopeOutput };
|
|
71
|
+
Object.assign(request, publicFields({ extract }));
|
|
72
|
+
request.arguments = parseCliOptions(command, options);
|
|
73
|
+
const { value: reply } = await runtime.run(request, { signal: connection.signal });
|
|
74
|
+
process.stdout.write(`${JSON.stringify(reply)}\n`);
|
|
75
|
+
process.exitCode = exitCodeFor(reply);
|
|
60
76
|
} catch (error) {
|
|
61
|
-
|
|
62
|
-
process.
|
|
77
|
+
const reply = publicFailure({ command, stage: 'validation', error, maxBytes });
|
|
78
|
+
process.stdout.write(`${JSON.stringify(reply)}\n`);
|
|
79
|
+
process.exitCode = exitCodeFor(reply);
|
|
63
80
|
} finally {
|
|
64
81
|
process.removeListener('SIGINT', disconnect);
|
|
65
82
|
process.removeListener('SIGTERM', disconnect);
|
|
66
83
|
}
|
|
67
84
|
}
|
|
68
85
|
|
|
86
|
+
// --extract and --output carry JSON. A flag without a value is a missing value,
|
|
87
|
+
// not `true`; the JSON string "null" is not null.
|
|
88
|
+
function publicFields(fields) {
|
|
89
|
+
const parsed = {};
|
|
90
|
+
for (const [name, raw] of Object.entries(fields)) {
|
|
91
|
+
if (raw === undefined) continue;
|
|
92
|
+
if (raw === true) throw new CommandError('missing_argument', `--${name} requires one JSON value${name === 'extract' ? '; use --extract null for this command\'s own result' : ''}.`, { field: name });
|
|
93
|
+
try { parsed[name] = JSON.parse(raw); }
|
|
94
|
+
catch { throw new CommandError('invalid_argument', `--${name} must be one JSON value${name === 'extract' ? ', for example null' : ''}: ${raw}`, { field: name }); }
|
|
95
|
+
}
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
function parseArgs(argv) {
|
|
70
100
|
const options = {};
|
|
71
101
|
let command = '';
|
|
102
|
+
let error;
|
|
103
|
+
// Finish reading named tokens after a lexical error so its reply can still
|
|
104
|
+
// honor a valid output budget. The first error rejects the entire request.
|
|
105
|
+
const append = (name, value) => {
|
|
106
|
+
try { appendOption(options, name, value); }
|
|
107
|
+
catch (cause) { error ||= cause; }
|
|
108
|
+
};
|
|
72
109
|
for (let index = 0; index < argv.length; index += 1) {
|
|
73
110
|
const arg = argv[index];
|
|
74
111
|
if (!arg.startsWith('--') && !command) {
|
|
@@ -76,22 +113,24 @@ function parseArgs(argv) {
|
|
|
76
113
|
continue;
|
|
77
114
|
}
|
|
78
115
|
if (!arg.startsWith('--')) {
|
|
79
|
-
|
|
116
|
+
error ||= new CommandError('unexpected_argument', `Unexpected positional argument: ${arg}. Supply values through named flags.`, { field: `argv[${index}]` });
|
|
117
|
+
continue;
|
|
80
118
|
}
|
|
81
119
|
const rawName = arg.slice(2);
|
|
82
120
|
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(rawName)) {
|
|
83
|
-
|
|
121
|
+
error ||= new CommandError('invalid_argument', `Invalid CLI flag: ${arg}. Use --kebab-case flags followed by separate value tokens.`, { field: `argv[${index}]` });
|
|
122
|
+
continue;
|
|
84
123
|
}
|
|
85
124
|
const name = rawName.replace(/-([a-z])/g, (_, value) => value.toUpperCase());
|
|
86
125
|
const next = argv[index + 1];
|
|
87
126
|
if (name === 'help' || next === undefined || next.startsWith('--')) {
|
|
88
|
-
|
|
127
|
+
append(name, true);
|
|
89
128
|
continue;
|
|
90
129
|
}
|
|
91
|
-
|
|
130
|
+
append(name, next);
|
|
92
131
|
index += 1;
|
|
93
132
|
}
|
|
94
|
-
return { command, options };
|
|
133
|
+
return { command, options, ...(error ? { error } : {}) };
|
|
95
134
|
}
|
|
96
135
|
|
|
97
136
|
function appendOption(options, name, value) {
|
package/bin/command-discovery.js
CHANGED
|
@@ -27,7 +27,19 @@ const commandDomains = {
|
|
|
27
27
|
};
|
|
28
28
|
const supportedTargetsText = `AI App Bridge supports ${supportedTargets.join('; ')}.`;
|
|
29
29
|
const commandDomainsText = `Command domains: ${Object.entries(commandDomains).map(([domain, summary]) => `${domain}(${summary})`).join('; ')}.`;
|
|
30
|
-
const discoveryText = 'Use capabilities with a domain for a command directory, then request one command and operation when applicable. For intent decide, narrow by the actual platform, provider and action.
|
|
30
|
+
const discoveryText = 'Use capabilities with a domain for a command directory, then request one command and operation when applicable. For intent decide, narrow by the actual platform, provider and action. Discovery output is limited to 96 KiB; narrow the query if a full schema or includeOptions:true exceeds this budget. Execute with run, command-specific arguments and explicit extract.';
|
|
31
|
+
|
|
32
|
+
function discoveryBudget(value) {
|
|
33
|
+
const limitBytes = require('./public-reply').DEFAULT_OUTPUT_BYTES;
|
|
34
|
+
const attemptedBytes = Buffer.byteLength(JSON.stringify(value));
|
|
35
|
+
if (attemptedBytes <= limitBytes) return value;
|
|
36
|
+
return { ok: false, error: 'discovery_output_too_large', attemptedBytes, limitBytes,
|
|
37
|
+
message: 'Request a command directory without includeOptions, then one command and operation. For intent decide, add platform/provider/action. No schema was truncated.',
|
|
38
|
+
filters: ['domain', 'command', ...schemaFilterNames], example: { command: 'intent', operation: 'start' } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function capabilities(args) { return discoveryBudget(capabilityPayload(args)); }
|
|
42
|
+
function commandHelpSchema(command, filters) { return discoveryBudget(commandInputSchema(command, filters)); }
|
|
31
43
|
function capabilityPayload(args = {}) {
|
|
32
44
|
if (!args || typeof args !== 'object' || Array.isArray(args)) return { ok: false, error: 'invalid_argument', field: 'arguments', dispatched: false, ambiguous: false };
|
|
33
45
|
const invalidKey = Object.keys(args).find(key => !['command', 'domain', 'includeOptions', ...schemaFilterNames].includes(key));
|
|
@@ -46,7 +58,7 @@ function capabilityPayload(args = {}) {
|
|
|
46
58
|
return {
|
|
47
59
|
ok: Boolean(definition),
|
|
48
60
|
command: requestedCommand,
|
|
49
|
-
...(definition ? shapeCommandDefinition(definition, true, filters) : { error: 'unknown_command' }),
|
|
61
|
+
...(definition ? shapeCommandDefinition(definition, true, filters) : { error: 'unknown_command', message: `Unknown command: ${requestedCommand}. Omit command, or pass a domain, to list the command directory.` }),
|
|
50
62
|
};
|
|
51
63
|
} catch (error) {
|
|
52
64
|
if (!(error instanceof CommandError)) throw error;
|
|
@@ -56,7 +68,10 @@ function capabilityPayload(args = {}) {
|
|
|
56
68
|
|
|
57
69
|
const requestedDomain = args.domain ? String(args.domain) : '';
|
|
58
70
|
const domains = {};
|
|
59
|
-
if (requestedDomain && !Object.hasOwn(commandDomains, requestedDomain))
|
|
71
|
+
if (requestedDomain && !Object.hasOwn(commandDomains, requestedDomain)) {
|
|
72
|
+
return { ok: false, error: 'unknown_domain', field: 'domain', dispatched: false, ambiguous: false,
|
|
73
|
+
message: `Unknown domain: ${requestedDomain}. Domains: ${Object.keys(commandDomains).join(', ')}. android is a platform, not a domain: platform, provider and action narrow intent decide schemas, e.g. {"command":"intent","operation":"decide","platform":"android","provider":"native","action":"tap"}.` };
|
|
74
|
+
}
|
|
60
75
|
for (const definition of [...isolatedCommandDefinitions, ...commandDefinitions]) {
|
|
61
76
|
if (requestedDomain && definition.domain !== requestedDomain) continue;
|
|
62
77
|
if (!domains[definition.domain]) domains[definition.domain] = [];
|
|
@@ -139,4 +154,4 @@ function shapeCommandDefinition(definition, includeOptions, filters = {}) {
|
|
|
139
154
|
|
|
140
155
|
|
|
141
156
|
function normalizeCommandName(value) { return typeof value === 'string' ? value : ''; }
|
|
142
|
-
module.exports = { capabilities
|
|
157
|
+
module.exports = { capabilities, commandInputSchema, commandHelpSchema, commandDomains, supportedTargets, supportedTargetsText, commandDomainsText, discoveryText };
|
package/bin/command-registry.js
CHANGED
|
@@ -8,6 +8,7 @@ const { executionCommandSchema, nativeGestureSchema, nativeSelector, flutterSele
|
|
|
8
8
|
const { flutterActionSchema, webCommandSchema } = require('./shared-kernel/provider-command-contracts');
|
|
9
9
|
|
|
10
10
|
const commandDefinitions = [
|
|
11
|
+
{ command: 'response', domain: 'evidence', summary: 'Read and re-extract one saved response using its unchanged control.source.ref. This never repeats the original action or refreshes an observation.', targetKind: 'host', options: ['operation', 'ref'] },
|
|
11
12
|
{ command: 'executor-prepare', domain: 'advanced', summary: 'Prepare optional test executors from the existing project: generate Android androidTest entry/dependencies and matching APKs, add Flutter dev dependency and build the generated entrypoint, prepare managed iOS WDA, or install pinned Playwright/browser. Host preparation does not install or launch a mobile application. No application ID is changed. Requires app.test in Script.', targetKind: 'host', options: ['platform'] },
|
|
12
13
|
{ command: 'ui-observation', domain: 'core', summary: 'Start a bounded UI observation window (100–5000 ms), inspect it, or stop its lease. Off by default; provider selects native or Flutter. New windows establish a fresh baseline.', targetApp: true, options: ['operation', 'provider', 'durationMs', 'leaseId', 'packageName', 'serial'] },
|
|
13
14
|
{ command: 'ios-ui-observation', domain: 'ios', summary: 'Control bounded iOS native or Flutter UI observation. Off by default; expires locally even if the Host exits.', targetKind: 'ios-app', options: ['operation', 'provider', 'durationMs', 'leaseId', 'deviceId', 'bundleId'] },
|
|
@@ -290,14 +291,23 @@ const workflowCommands = new Set(['install-apk', 'permission-dialog']);
|
|
|
290
291
|
const permissionCommands = new Set(['permission-state', 'permission-grant', 'permission-revoke']);
|
|
291
292
|
const expertCommands = new Set(['uia-runtime', 'freeze-app', 'thaw-app', 'flutter-action', 'h5-eval', 'flutter-h5-eval', 'ios-h5-eval', 'ios-flutter-action', 'ios-setup', 'web-command', 'forward', 'remove-forward', 'webview-pages', 'appops-set', 'permission-grant', 'permission-revoke']);
|
|
292
293
|
const deviceCommands = new Set(['uia-tree', 'screenshot', 'tap', 'tap-uia-text', 'keyboard-state', 'hide-keyboard', 'swipe', 'keyevent', 'logcat', 'permission-dialog', 'install-apk', 'remove-forward']);
|
|
294
|
+
// Discovery is a recorded misuse of run: point at the discovery entry instead of
|
|
295
|
+
// only rejecting the name.
|
|
296
|
+
function unknownCommand(command) {
|
|
297
|
+
const message = command === 'capabilities'
|
|
298
|
+
? 'capabilities is the discovery tool, not a run command. Call the capabilities tool (CLI: --help <command>) with an optional domain or command filter, then run one of the listed command names.'
|
|
299
|
+
: `Unknown command: ${command}. List command names with the capabilities tool (CLI: ai-app-bridge help).`;
|
|
300
|
+
return new CommandError('unknown_command', message, { field: 'command' });
|
|
301
|
+
}
|
|
302
|
+
|
|
293
303
|
function commandContract(command) {
|
|
294
304
|
const definition = commandByName.get(command) || isolatedByName.get(command);
|
|
295
|
-
if (!definition) throw
|
|
305
|
+
if (!definition) throw unknownCommand(command);
|
|
296
306
|
const isolated = isolatedByName.has(command);
|
|
297
|
-
const platform = ['runtime', 'executor-prepare'].includes(command) ? 'host' : isolated ? (command === 'evidence' ? 'host' : 'multi')
|
|
307
|
+
const platform = ['runtime', 'executor-prepare', 'response'].includes(command) ? 'host' : isolated ? (command === 'evidence' ? 'host' : 'multi')
|
|
298
308
|
: definition.domain === 'ios' ? 'ios' : definition.domain === 'web' ? 'web' : 'android';
|
|
299
309
|
const role = command === 'runtime' || command === 'intent' || command === 'script' || workflowCommands.has(command) ? 'execution'
|
|
300
|
-
:
|
|
310
|
+
: ['evidence', 'response'].includes(command) ? 'evidence' : expertCommands.has(command) ? 'expert' : 'capability';
|
|
301
311
|
const permission = scriptPermissionByCommand.get(command) || null;
|
|
302
312
|
return {
|
|
303
313
|
role, platform,
|
|
@@ -319,7 +329,8 @@ function commandContract(command) {
|
|
|
319
329
|
|
|
320
330
|
function commandSchema(command) {
|
|
321
331
|
const definition = commandByName.get(command) || isolatedByName.get(command);
|
|
322
|
-
if (!definition) throw
|
|
332
|
+
if (!definition) throw unknownCommand(command);
|
|
333
|
+
if (command === 'response') return require('./response-store').responseSchema();
|
|
323
334
|
if (command === 'executor-prepare') return require('./executors/preparation').preparationSchema();
|
|
324
335
|
if (require('./ui-observation').commands.has(command)) return require('./ui-observation').schema(command, optionTypes);
|
|
325
336
|
if (command === 'web-executor') return require('./executors/command-schema').webExecutorSchema();
|
package/bin/command-request.js
CHANGED
|
@@ -2,13 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
const { CommandError } = require('./command-errors');
|
|
4
4
|
const { validateCommandArguments } = require('./command-registry');
|
|
5
|
+
const { validateValue } = require('./shared-kernel/argument-schema');
|
|
6
|
+
|
|
7
|
+
// The public run request shared by MCP and the CLI. Business arguments stay
|
|
8
|
+
// under `arguments`; `extract` and `output` never reach a device action.
|
|
9
|
+
const extractSchema = {
|
|
10
|
+
description: 'Required. Use null for this command\'s result within the output budget, or regex/script to extract a smaller result on the Host. Execution and control facts remain separate. On overflow or extraction failure, use a returned response ref to retry extraction; do not repeat the action.',
|
|
11
|
+
oneOf: [
|
|
12
|
+
{ type: 'null' },
|
|
13
|
+
{
|
|
14
|
+
type: 'object', additionalProperties: false,
|
|
15
|
+
required: ['mode', 'pattern', 'inputPath'],
|
|
16
|
+
properties: {
|
|
17
|
+
mode: { const: 'regex' },
|
|
18
|
+
pattern: { type: 'string', minLength: 1, maxLength: 2048 },
|
|
19
|
+
flags: { type: 'string', pattern: '^[imsu]*$', description: 'Unique JavaScript RegExp flags. The Host returns all matches; g/y are not accepted.' },
|
|
20
|
+
inputPath: { type: 'string', description: 'JSON Pointer to a string in the response. Empty string selects a text response.' },
|
|
21
|
+
timeoutMs: { type: 'integer', minimum: 1, maximum: 10000 },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: 'object', additionalProperties: false,
|
|
26
|
+
required: ['mode', 'language'],
|
|
27
|
+
properties: {
|
|
28
|
+
mode: { const: 'script' },
|
|
29
|
+
language: { enum: ['javascript', 'python'] },
|
|
30
|
+
source: { type: 'string', minLength: 1, maxLength: 65536, description: 'main(ctx). ctx.inputs contains kind, response, execution, control. Return a JSON value.' },
|
|
31
|
+
sourcePath: { type: 'string', minLength: 1, description: 'Resolve from the caller cwd and freeze source before dispatch. Exactly one of source/sourcePath.' },
|
|
32
|
+
timeoutMs: { type: 'integer', minimum: 1, maximum: 10000 },
|
|
33
|
+
},
|
|
34
|
+
oneOf: [{ required: ['source'] }, { required: ['sourcePath'] }],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
const outputSchema = {
|
|
39
|
+
type: 'object', additionalProperties: false, required: ['maxBytes'],
|
|
40
|
+
properties: { maxBytes: { type: 'integer', minimum: 16384, maximum: 262144 } },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function publicRequestSchema() {
|
|
44
|
+
return {
|
|
45
|
+
type: 'object', additionalProperties: false, required: ['command', 'extract'],
|
|
46
|
+
properties: {
|
|
47
|
+
command: { type: 'string', minLength: 1 },
|
|
48
|
+
arguments: { type: 'object', additionalProperties: true },
|
|
49
|
+
extract: extractSchema,
|
|
50
|
+
output: outputSchema,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Preserve a valid caller budget even when another part of the request fails
|
|
56
|
+
// validation. Invalid output is still reported by validateRunRequest.
|
|
57
|
+
function publicOutputLimit(args) {
|
|
58
|
+
if (!args || !Object.hasOwn(args, 'output')) return undefined;
|
|
59
|
+
try { validateValue(args.output, outputSchema, 'output'); }
|
|
60
|
+
catch { return undefined; }
|
|
61
|
+
return args.output.maxBytes;
|
|
62
|
+
}
|
|
5
63
|
|
|
6
64
|
function validateRunRequest(args) {
|
|
7
65
|
if (!args || typeof args !== 'object' || Array.isArray(args)) throw new CommandError('invalid_argument', 'run requires an object.', { field: 'arguments' });
|
|
8
|
-
const extra = Object.keys(args).find(key =>
|
|
9
|
-
if (extra) throw new CommandError('unsupported_argument', 'Put all command parameters in run.arguments.', { field: extra });
|
|
66
|
+
const extra = Object.keys(args).find(key => !['command', 'arguments', 'extract', 'output'].includes(key));
|
|
67
|
+
if (extra) throw new CommandError('unsupported_argument', 'Put all command parameters in run.arguments; only command, arguments, extract and output are top-level fields.', { field: extra });
|
|
68
|
+
if (!Object.hasOwn(args, 'extract')) {
|
|
69
|
+
throw new CommandError('missing_argument', 'extract is required. Pass null (CLI: --extract null) to deliver this command\'s own result, or a regex/script extraction. Nothing was dispatched.', { field: 'extract' });
|
|
70
|
+
}
|
|
71
|
+
validateValue(args.extract, extractSchema, 'extract');
|
|
72
|
+
if (Object.hasOwn(args, 'output')) validateValue(args.output, outputSchema, 'output');
|
|
10
73
|
const command = typeof args.command === 'string' ? args.command : '';
|
|
11
|
-
return { command, arguments: validateCommandArguments(command, args.arguments === undefined ? {} : args.arguments)
|
|
74
|
+
return { command, arguments: validateCommandArguments(command, args.arguments === undefined ? {} : args.arguments), extract: args.extract,
|
|
75
|
+
...(args.output ? { output: args.output } : {}) };
|
|
12
76
|
}
|
|
13
77
|
|
|
14
|
-
module.exports = { validateRunRequest };
|
|
78
|
+
module.exports = { validateRunRequest, publicRequestSchema, publicOutputLimit, extractSchema, outputSchema };
|
package/bin/execution-host.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { executeProviderCommand } = require('./device-provider');
|
|
4
|
-
const { validateRunRequest } = require('./command-request');
|
|
4
|
+
const { validateRunRequest, publicOutputLimit } = require('./command-request');
|
|
5
5
|
const { resolveCommandPaths } = require('./shared-kernel/request-context');
|
|
6
6
|
const { commandFailure, normalizeCommandResult } = require('./command-errors');
|
|
7
|
+
const { publicReply, publicFailure, responseReply, finishReply, boundedReply } = require('./public-reply');
|
|
8
|
+
const { prepareExtraction } = require('./extraction/prepare');
|
|
7
9
|
const { runExecution, withoutExecution } = require('./shared-kernel/execution-scope');
|
|
8
10
|
const { getHostFactStore } = require('./shared-kernel/host-fact-store');
|
|
9
11
|
const { FactRecorder, historyDescriptor, isMobileCaptureCommand } = require('./fact-recorder');
|
|
@@ -35,8 +37,9 @@ let closing = false;
|
|
|
35
37
|
const activeRuns = new Set();
|
|
36
38
|
|
|
37
39
|
// Callers share one execution host. The owning process controls its lifecycle.
|
|
40
|
+
// Every reply is the public reply of the request, assembled exactly once here.
|
|
38
41
|
function run(args, dependencies) {
|
|
39
|
-
if (closing) return Promise.resolve(valueReply({ ok: false, error: 'runtime_stopping', dispatched: false, ambiguous: false }));
|
|
42
|
+
if (closing) return Promise.resolve({ value: boundedReply(publicReply({ command: requestedCommand(args), reply: valueReply({ ok: false, error: 'runtime_stopping', dispatched: false, ambiguous: false }) })) });
|
|
40
43
|
const task = runGeneric(args, dependencies);
|
|
41
44
|
activeRuns.add(task);
|
|
42
45
|
task.then(() => activeRuns.delete(task), () => activeRuns.delete(task));
|
|
@@ -147,11 +150,30 @@ function wrapIsolatedEntry(entry, namespace) {
|
|
|
147
150
|
};
|
|
148
151
|
}
|
|
149
152
|
|
|
153
|
+
function requestedCommand(args) {
|
|
154
|
+
return typeof args?.command === 'string' ? args.command : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
150
157
|
async function runGeneric(args = {}, dependencies = {}) {
|
|
158
|
+
const command = requestedCommand(args);
|
|
159
|
+
const maxBytes = publicOutputLimit(args);
|
|
160
|
+
let request;
|
|
161
|
+
let commandArguments;
|
|
162
|
+
let extract;
|
|
163
|
+
try {
|
|
164
|
+
request = validateRunRequest(args);
|
|
165
|
+
commandArguments = resolveCommandPaths(request.command, request.arguments);
|
|
166
|
+
extract = prepareExtraction(request.extract);
|
|
167
|
+
} catch (error) { return { value: publicFailure({ command, stage: 'validation', error, maxBytes }) }; }
|
|
151
168
|
try {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
169
|
+
const getStore = () => dependencies.responseStore || getResponseStore();
|
|
170
|
+
if (command === 'response') {
|
|
171
|
+
const frozen = getStore().read(commandArguments.ref);
|
|
172
|
+
return { value: await finishReply({ body: responseReply(frozen), extract, output: request.output, frozen }) };
|
|
173
|
+
}
|
|
174
|
+
const reply = await commandRouter.route(request.command, commandArguments, dependencies);
|
|
175
|
+
return { value: await finishReply({ body: publicReply({ command: request.command, reply, completed: true }), extract, output: request.output, getStore }) };
|
|
176
|
+
} catch (error) { return { value: publicFailure({ command: request.command, stage: 'execution', error, maxBytes: request.output?.maxBytes }) }; }
|
|
155
177
|
}
|
|
156
178
|
|
|
157
179
|
async function runCommand(command, args = {}, dependencies = {}) {
|
|
@@ -538,6 +560,10 @@ function getSharedFactStore() {
|
|
|
538
560
|
return sharedFactStore;
|
|
539
561
|
}
|
|
540
562
|
|
|
563
|
+
function getResponseStore() {
|
|
564
|
+
return require('./response-store').createResponseStore({ adapter: createSegmentedEvidenceAdapter(getSharedFactStore()) });
|
|
565
|
+
}
|
|
566
|
+
|
|
541
567
|
function getSharedFactRecorder() {
|
|
542
568
|
if (String(process.env.AI_APP_BRIDGE_FACT_CACHE || '').toLowerCase() === 'off') return null;
|
|
543
569
|
if (sharedFactRecorder) return sharedFactRecorder;
|
package/bin/execution-runtime.js
CHANGED
|
@@ -6,6 +6,7 @@ const path = require('node:path');
|
|
|
6
6
|
const http = require('node:http');
|
|
7
7
|
const { randomBytes, randomUUID } = require('node:crypto');
|
|
8
8
|
const { CommandError, commandFailure } = require('./command-errors');
|
|
9
|
+
const { publicFailure } = require('./public-reply');
|
|
9
10
|
const { protocol, maxMessageBytes, encodeReply, readJson } = require('./runtime-protocol');
|
|
10
11
|
const { runtimeLocation, runtimeIdentity, acquireRuntimeLock, publishEndpoint } = require('./runtime-directory');
|
|
11
12
|
const { inRequestDirectory } = require('./shared-kernel/request-context');
|
|
@@ -24,21 +25,28 @@ async function start() {
|
|
|
24
25
|
const status = () => ({ ok: true, command: 'runtime', status: phase, runtimeId, pid: process.pid, identity,
|
|
25
26
|
startedAtMs, facts: location.facts, profile: location.profile, node: { executable: process.execPath, version: process.version } });
|
|
26
27
|
const server = http.createServer(async (request, response) => {
|
|
28
|
+
// Execute replies are public replies; a failure of the execute path itself
|
|
29
|
+
// is wrapped the same way. Status and stop keep their internal shape.
|
|
30
|
+
let message = null;
|
|
31
|
+
const executing = () => message?.method === 'execute';
|
|
32
|
+
const commandOf = () => (typeof message?.arguments?.command === 'string' ? message.arguments.command : undefined);
|
|
27
33
|
const send = reply => {
|
|
28
34
|
if (response.destroyed) return;
|
|
29
|
-
let
|
|
30
|
-
try {
|
|
31
|
-
catch (error) {
|
|
32
|
-
if (Buffer.byteLength(
|
|
33
|
-
|
|
35
|
+
let body;
|
|
36
|
+
try { body = JSON.stringify({ protocol, runtimeId, reply: encodeReply(reply) }); }
|
|
37
|
+
catch (error) { body = JSON.stringify({ protocol, runtimeId, reply: encodeReply(failure(error)) }); }
|
|
38
|
+
if (Buffer.byteLength(body) > maxMessageBytes) {
|
|
39
|
+
body = JSON.stringify({ protocol, runtimeId, reply: encodeReply(failure(new CommandError('runtime_message_too_large', 'Runtime messages are limited to 128 MiB.', { dispatched: null, ambiguous: true }))) });
|
|
40
|
+
}
|
|
34
41
|
response.writeHead(200, { 'Content-Type': 'application/json', Connection: 'close' });
|
|
35
|
-
response.end(
|
|
42
|
+
response.end(body);
|
|
36
43
|
};
|
|
44
|
+
const failure = error => (executing() ? { value: publicFailure({ command: commandOf(), stage: 'execution', error }) } : { value: commandFailure(error) });
|
|
37
45
|
try {
|
|
38
46
|
if (request.headers.authorization !== `Bearer ${token}` || request.method !== 'POST' || request.url !== '/rpc') {
|
|
39
47
|
request.resume(); throw new CommandError('runtime_access_denied', 'The local runtime connection is not authenticated.');
|
|
40
48
|
}
|
|
41
|
-
|
|
49
|
+
message = await readJson(request);
|
|
42
50
|
if (!message || typeof message !== 'object' || Array.isArray(message)
|
|
43
51
|
|| Object.keys(message).some(key => !['protocol', 'runtimeId', 'method', 'identity', 'arguments', 'cwd'].includes(key))
|
|
44
52
|
|| message.protocol !== protocol || message.runtimeId !== runtimeId) throw new CommandError('runtime_protocol_error', 'Runtime request identity or fields are invalid.');
|
|
@@ -46,8 +54,7 @@ async function start() {
|
|
|
46
54
|
if (message.method === 'stop') { await stop(); send({ value: status() }); return; }
|
|
47
55
|
if (message.method !== 'execute') throw new CommandError('runtime_protocol_error', 'Unknown runtime method.');
|
|
48
56
|
if (phase !== 'running') throw new CommandError('runtime_stopping', 'The runtime is stopping.');
|
|
49
|
-
|
|
50
|
-
if (message.identity?.config !== identity.config) throw new CommandError('runtime_configuration_mismatch', 'The runtime and client use different configuration.');
|
|
57
|
+
require('./runtime-directory').requireCompatible({ identity, runtimeId, pid: process.pid }, message.identity);
|
|
51
58
|
let validDirectory = false;
|
|
52
59
|
if (typeof message.cwd === 'string' && path.isAbsolute(message.cwd)) {
|
|
53
60
|
try { validDirectory = fs.statSync(message.cwd).isDirectory(); }
|
|
@@ -57,7 +64,7 @@ async function start() {
|
|
|
57
64
|
// Closing this HTTP connection never cancels the admitted work. Its
|
|
58
65
|
// original runtime, deadline and explicit cancel operation own settlement.
|
|
59
66
|
send(await inRequestDirectory(message.cwd, () => host.run(message.arguments)));
|
|
60
|
-
} catch (error) { send(
|
|
67
|
+
} catch (error) { send(failure(error)); }
|
|
61
68
|
});
|
|
62
69
|
server.requestTimeout = 30000;
|
|
63
70
|
server.headersTimeout = 10000;
|