@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/bin/runtime-client.js
CHANGED
|
@@ -4,10 +4,11 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const http = require('node:http');
|
|
6
6
|
const { fork } = require('node:child_process');
|
|
7
|
-
const { CommandError
|
|
8
|
-
const { validateRunRequest } = require('./command-request');
|
|
7
|
+
const { CommandError } = require('./command-errors');
|
|
8
|
+
const { validateRunRequest, publicOutputLimit } = require('./command-request');
|
|
9
|
+
const { publicReply, publicFailure, isPublicReply, finishReply } = require('./public-reply');
|
|
9
10
|
const { protocol, maxMessageBytes, readJson, decodeReply } = require('./runtime-protocol');
|
|
10
|
-
const { runtimeLocation, runtimeIdentity, acquireRuntimeLock, readEndpoint, prepareDirectory } = require('./runtime-directory');
|
|
11
|
+
const { runtimeLocation, runtimeIdentity, requireCompatible, acquireRuntimeLock, readEndpoint, prepareDirectory } = require('./runtime-directory');
|
|
11
12
|
const { executablePath } = require('./shared-kernel/executable-path');
|
|
12
13
|
const starting = new Map();
|
|
13
14
|
|
|
@@ -66,11 +67,6 @@ async function probe(location, signal) {
|
|
|
66
67
|
return { state: endpoint ? 'unresponsive' : 'starting', cause };
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
function requireCompatible(status, expected) {
|
|
70
|
-
if (status.identity.code !== expected.code) throw new CommandError('runtime_code_mismatch', 'The running runtime uses different code. Inspect runtime status and explicitly stop it before starting this build.', { details: { runtimeId: status.runtimeId, pid: status.pid } });
|
|
71
|
-
if (status.identity.config !== expected.config) throw new CommandError('runtime_configuration_mismatch', 'The running runtime uses different persistent/provider configuration. Use the same configuration or explicitly stop it first.', { details: { runtimeId: status.runtimeId, pid: status.pid } });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
70
|
function launch(location) {
|
|
75
71
|
prepareDirectory(location);
|
|
76
72
|
const log = fs.openSync(location.logFile, 'a', 0o600);
|
|
@@ -132,27 +128,48 @@ async function ensureRuntime(location, identity, signal) {
|
|
|
132
128
|
return starting.get(location.directory);
|
|
133
129
|
}
|
|
134
130
|
|
|
131
|
+
// The reply is always the public reply of the request. Executed commands
|
|
132
|
+
// arrive assembled by the Runtime; local paths and client failures are
|
|
133
|
+
// assembled here with the same module.
|
|
135
134
|
async function run(request, { signal } = {}) {
|
|
135
|
+
const command = typeof request?.command === 'string' ? request.command : undefined;
|
|
136
|
+
const maxBytes = publicOutputLimit(request);
|
|
137
|
+
let extract;
|
|
136
138
|
try {
|
|
137
139
|
request = validateRunRequest(request);
|
|
140
|
+
if (request.command === 'runtime' || (request.command === 'evidence' && request.arguments.operation === 'verify')) {
|
|
141
|
+
extract = require('./extraction/prepare').prepareExtraction(request.extract);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) { return { value: publicFailure({ command, stage: 'validation', error, maxBytes }) }; }
|
|
145
|
+
const local = async reply => ({ value: await finishReply({ body: publicReply({ command: request.command, reply, completed: true }),
|
|
146
|
+
extract, output: request.output }) });
|
|
147
|
+
try {
|
|
138
148
|
// Verification is an offline command in both transports; it neither
|
|
139
149
|
// opens FactStore nor depends on a running owner or valid store profile.
|
|
140
150
|
if (request.command === 'evidence' && request.arguments.operation === 'verify') {
|
|
141
|
-
return { value: await require('./shared-kernel/evidence-archive').handle(request.arguments) };
|
|
151
|
+
return local({ value: await require('./shared-kernel/evidence-archive').handle(request.arguments) });
|
|
142
152
|
}
|
|
143
153
|
const location = runtimeLocation();
|
|
144
154
|
const identity = runtimeIdentity(location);
|
|
145
155
|
if (request.command === 'runtime' && request.arguments.operation !== 'start') {
|
|
146
156
|
const state = await probe(location, signal);
|
|
147
|
-
if (state.state === 'stopped') return { value: { ok: true, command: 'runtime', status: 'stopped', facts: location.facts } };
|
|
157
|
+
if (state.state === 'stopped') return local({ value: { ok: true, command: 'runtime', status: 'stopped', facts: location.facts } });
|
|
148
158
|
if (state.state !== 'running') throw new CommandError('runtime_unresponsive', 'A runtime owns the OS lock but has not answered. It was not restarted.');
|
|
149
|
-
if (request.arguments.operation === 'stop') return await exchange(state.endpoint, 'stop', {}, { signal });
|
|
150
|
-
return { value: { ...state.status, compatible: state.status.identity.code === identity.code && state.status.identity.config === identity.config } };
|
|
159
|
+
if (request.arguments.operation === 'stop') return local(await exchange(state.endpoint, 'stop', {}, { signal }));
|
|
160
|
+
return local({ value: { ...state.status, compatible: state.status.identity.code === identity.code && state.status.identity.config === identity.config } });
|
|
151
161
|
}
|
|
152
162
|
const endpoint = await ensureRuntime(location, identity, signal);
|
|
153
|
-
if (request.command === 'runtime') return await exchange(endpoint, 'status', {}, { signal });
|
|
154
|
-
|
|
155
|
-
|
|
163
|
+
if (request.command === 'runtime') return local(await exchange(endpoint, 'status', {}, { signal }));
|
|
164
|
+
const reply = await exchange(endpoint, 'execute', request, { identity, signal });
|
|
165
|
+
if (isPublicReply(reply.value)) return reply;
|
|
166
|
+
// Authentication and request-decoding failures precede execute dispatch,
|
|
167
|
+
// so the internal RPC cannot yet identify the requested public command.
|
|
168
|
+
if (reply.value?.ok === false && reply.value.dispatched === false
|
|
169
|
+
&& ['runtime_access_denied', 'runtime_protocol_error', 'runtime_message_too_large'].includes(reply.value.error)) return { value: publicFailure({
|
|
170
|
+
command: request.command, stage: 'execution', error: new CommandError(reply.value.error, reply.value.message), maxBytes: request.output?.maxBytes }) };
|
|
171
|
+
throw new CommandError('runtime_protocol_error', 'Execute returned no public reply.', { dispatched: null, ambiguous: true });
|
|
172
|
+
} catch (error) { return { value: publicFailure({ command: request.command, stage: 'execution', error, maxBytes: request.output?.maxBytes }) }; }
|
|
156
173
|
}
|
|
157
174
|
|
|
158
175
|
module.exports = { run, exchange };
|
package/bin/runtime-directory.js
CHANGED
|
@@ -11,9 +11,7 @@ const { protocol } = require('./runtime-protocol');
|
|
|
11
11
|
const { canonicalPath } = require('./shared-kernel/canonical-path');
|
|
12
12
|
const { executablePath } = require('./shared-kernel/executable-path');
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
function codeFingerprint() {
|
|
16
|
-
if (fingerprint) return fingerprint;
|
|
14
|
+
function calculateCodeFingerprint() {
|
|
17
15
|
const hash = createHash('sha256');
|
|
18
16
|
function visit(directory) {
|
|
19
17
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
@@ -33,12 +31,19 @@ function codeFingerprint() {
|
|
|
33
31
|
}
|
|
34
32
|
const nativeDirectory = path.dirname(require.resolve('@mobileaidev/segmented-fact-store-native'));
|
|
35
33
|
hash.update(fs.readFileSync(path.join(nativeDirectory, 'index.js')));
|
|
36
|
-
hash.update(fs.readFileSync(path.join(nativeDirectory, '
|
|
34
|
+
hash.update(fs.readFileSync(path.join(nativeDirectory, 'binding-path.js')));
|
|
35
|
+
hash.update(fs.readFileSync(path.join(nativeDirectory, 'prebuilds/manifest.json')));
|
|
36
|
+
const nativeBinding = require('@mobileaidev/segmented-fact-store-native/binding-path').resolveBinding();
|
|
37
|
+
hash.update(fs.readFileSync(nativeBinding.path));
|
|
37
38
|
hash.update(JSON.stringify({ node: process.versions.node, modules: process.versions.modules }));
|
|
38
|
-
|
|
39
|
-
return fingerprint;
|
|
39
|
+
return hash.digest('hex');
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// Pin the running client's code when this entrypoint loads, before the first
|
|
43
|
+
// request. Replacing installed files cannot make this process claim new code.
|
|
44
|
+
const fingerprint = calculateCodeFingerprint();
|
|
45
|
+
const packageVersion = require('../package.json').version;
|
|
46
|
+
|
|
42
47
|
function runtimeLocation() {
|
|
43
48
|
const target = hostFactStoreTarget();
|
|
44
49
|
const facts = canonicalPath(target.directory);
|
|
@@ -55,7 +60,31 @@ function runtimeIdentity(location = runtimeLocation()) {
|
|
|
55
60
|
const config = { facts: location.facts, profile: location.profile, ownership: canonicalPath(ownershipDirectory()),
|
|
56
61
|
adb: executablePath(process.env.ADB || 'adb') ?? { unavailable: process.env.ADB || 'adb' },
|
|
57
62
|
environment: Object.fromEntries(names.map(name => [name, process.env[name] ?? null])) };
|
|
58
|
-
return { protocol, code:
|
|
63
|
+
return { protocol, code: fingerprint, config: createHash('sha256').update(JSON.stringify(config)).digest('hex'),
|
|
64
|
+
version: packageVersion, node: { version: process.versions.node, modules: process.versions.modules, napi: process.versions.napi } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function requireCompatible(status, client) {
|
|
68
|
+
const runtime = status.identity;
|
|
69
|
+
const codeMismatch = runtime?.code !== client?.code;
|
|
70
|
+
const configMismatch = runtime?.config !== client?.config;
|
|
71
|
+
if (!codeMismatch && !configMismatch) return;
|
|
72
|
+
const parse = version => typeof version === 'string' && /^\d+\.\d+\.\d+$/.test(version) ? version.split('.').map(Number) : null;
|
|
73
|
+
const runtimeVersion = parse(runtime?.version), clientVersion = parse(client?.version);
|
|
74
|
+
let outdatedSide = 'undetermined';
|
|
75
|
+
if (runtimeVersion && clientVersion) {
|
|
76
|
+
const index = runtimeVersion.findIndex((part, i) => part !== clientVersion[i]);
|
|
77
|
+
if (index >= 0) outdatedSide = runtimeVersion[index] < clientVersion[index] ? 'runtime' : 'client';
|
|
78
|
+
}
|
|
79
|
+
const describe = identity => `package ${identity?.version ?? 'unknown'}, Node ${identity?.node?.version ?? 'unknown'}, ABI ${identity?.node?.modules ?? 'unknown'}, code ${identity?.code ?? 'unknown'}, config ${identity?.config ?? 'unknown'}`;
|
|
80
|
+
let next;
|
|
81
|
+
if (!codeMismatch) next = 'Use matching persistent/provider configuration; the running Runtime was not restarted.';
|
|
82
|
+
else if (outdatedSide === 'client') next = 'The client package is older. Update and reconnect the client; do not stop the newer Runtime to fix this client.';
|
|
83
|
+
else if (outdatedSide === 'runtime') next = 'The Runtime package is older. Finish its active tasks, then explicitly run runtime --operation stop --extract null and reconnect with the updated installation.';
|
|
84
|
+
else next = 'Builds or Node environments differ. Align the installations and reconnect; fingerprints alone cannot identify an older side.';
|
|
85
|
+
throw new CommandError(codeMismatch ? 'runtime_code_mismatch' : 'runtime_configuration_mismatch',
|
|
86
|
+
`Client: ${describe(client)}. Runtime: ${describe(runtime)}. ${next}`,
|
|
87
|
+
{ details: { runtimeId: status.runtimeId, pid: status.pid, client, runtime, outdatedSide } });
|
|
59
88
|
}
|
|
60
89
|
|
|
61
90
|
function prepareDirectory(location) {
|
|
@@ -106,4 +135,4 @@ function publishEndpoint(location, endpoint) {
|
|
|
106
135
|
fs.renameSync(temporary, location.endpointFile);
|
|
107
136
|
}
|
|
108
137
|
|
|
109
|
-
module.exports = { runtimeLocation, runtimeIdentity, prepareDirectory, acquireRuntimeLock, readEndpoint, publishEndpoint };
|
|
138
|
+
module.exports = { runtimeLocation, runtimeIdentity, requireCompatible, prepareDirectory, acquireRuntimeLock, readEndpoint, publishEndpoint };
|
|
@@ -38,174 +38,190 @@ function createChildRuntimeAdapter({
|
|
|
38
38
|
onSpawnFailure,
|
|
39
39
|
} = {}) {
|
|
40
40
|
let channel = null;
|
|
41
|
+
let child = null;
|
|
41
42
|
let exited = Promise.resolve();
|
|
42
43
|
let readControl = () => ({ status: 'running', pauseReason: null });
|
|
43
44
|
|
|
44
45
|
async function terminate() {
|
|
45
46
|
if (channel) channel.stop();
|
|
46
47
|
await exited;
|
|
48
|
+
child?.stdin?.destroy(); child?.stdout?.destroy(); child?.stderr?.destroy();
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
function start(args) {
|
|
50
|
-
|
|
51
|
+
async function start(args) {
|
|
52
|
+
const result = await inRequestDirectory(args.spec.cwd ?? requestDirectory(), () => startInDirectory(args));
|
|
53
|
+
if (!result.ok && channel) {
|
|
54
|
+
const diagnostics = { ...channel.diagnostics(), ...result.diagnostics };
|
|
55
|
+
if (Object.keys(diagnostics).length) result.diagnostics = diagnostics;
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
async function startInDirectory({ spec, host, agent, emit, control }) {
|
|
54
61
|
readControl = typeof control === 'function' ? control : () => control || { status: 'running', pauseReason: null };
|
|
55
|
-
|
|
56
|
-
const artifact = path.join(directory, `main${extension}`);
|
|
57
|
-
fs.writeFileSync(artifact, spec.source);
|
|
58
|
-
const sdk = path.join(__dirname, sdkFile);
|
|
59
|
-
const child = spawn(executable, argsPrefix.concat([sdk, artifact]), {
|
|
60
|
-
shell: false,
|
|
61
|
-
cwd: requestDirectory(),
|
|
62
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
63
|
-
});
|
|
64
|
-
channel = createScriptSessionChannel(child, {
|
|
65
|
-
maxFrameBytes: Math.max(spec.policy.maxOutputBytes, spec.policy.maxProgressBytes, 1024 * 1024) + 64 * 1024,
|
|
66
|
-
});
|
|
67
|
-
exited = childExitPromise(child, onSpawnFailure);
|
|
68
|
-
const finished = exited;
|
|
62
|
+
let directory;
|
|
69
63
|
let timer = null;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
73
|
-
const rpc = pending => Promise.race([pending, timedOut, finished.then(() => { throw new Error('child_crashed'); })]);
|
|
64
|
+
channel = null;
|
|
65
|
+
exited = Promise.resolve();
|
|
74
66
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
95
|
-
while (true) {
|
|
96
|
-
const message = await Promise.race([
|
|
67
|
+
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'aab-script-'));
|
|
68
|
+
const artifact = path.join(directory, `main${extension}`);
|
|
69
|
+
fs.writeFileSync(artifact, spec.source);
|
|
70
|
+
const sdk = path.join(__dirname, sdkFile);
|
|
71
|
+
child = spawn(executable, argsPrefix.concat([sdk, artifact]), {
|
|
72
|
+
shell: false,
|
|
73
|
+
cwd: requestDirectory(),
|
|
74
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
75
|
+
});
|
|
76
|
+
channel = createScriptSessionChannel(child, {
|
|
77
|
+
maxFrameBytes: Math.max(spec.policy.maxOutputBytes, spec.policy.maxProgressBytes, 1024 * 1024) + 64 * 1024,
|
|
78
|
+
});
|
|
79
|
+
exited = childExitPromise(child, onSpawnFailure);
|
|
80
|
+
const finished = exited;
|
|
81
|
+
const timedOut = new Promise((resolve) => {
|
|
82
|
+
timer = setTimeout(() => resolve({ type: 'timeout' }), spec.policy.timeoutMs);
|
|
83
|
+
});
|
|
84
|
+
const rpc = pending => Promise.race([pending, timedOut, finished.then(() => { throw new Error('child_crashed'); })]);
|
|
85
|
+
const ready = await Promise.race([
|
|
97
86
|
channel.nextMessage(),
|
|
98
87
|
finished,
|
|
99
88
|
timedOut,
|
|
100
89
|
]);
|
|
101
|
-
if (
|
|
90
|
+
if (ready && ready.type === 'timeout') {
|
|
102
91
|
await terminate();
|
|
103
|
-
emit('script_failed', { error: 'timeout' });
|
|
104
92
|
return { ok: false, error: 'timeout' };
|
|
105
93
|
}
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
await terminate();
|
|
110
|
-
emit('script_failed', { error: 'timeout' });
|
|
111
|
-
return { ok: false, error: 'timeout' };
|
|
112
|
-
}
|
|
113
|
-
channel.send({ type: 'result', id: message.id, value, control: readControl() });
|
|
114
|
-
continue;
|
|
94
|
+
if (!ready || ready.type !== 'ready') {
|
|
95
|
+
await terminate();
|
|
96
|
+
return { ok: false, error: ready && ready.type === 'fail' && ready.error === 'handshake_failed' ? 'handshake_failed' : 'child_crashed' };
|
|
115
97
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
98
|
+
emit('child_started', { kind });
|
|
99
|
+
channel.send({
|
|
100
|
+
type: 'start',
|
|
101
|
+
inputs: spec.inputs,
|
|
102
|
+
entrypoint: spec.entrypoint,
|
|
103
|
+
sourceName: spec.sourcePath || artifact,
|
|
104
|
+
control: control(),
|
|
105
|
+
});
|
|
106
|
+
while (true) {
|
|
107
|
+
const message = await Promise.race([
|
|
108
|
+
channel.nextMessage(),
|
|
109
|
+
finished,
|
|
110
|
+
timedOut,
|
|
111
|
+
]);
|
|
112
|
+
if (message.type === 'timeout') {
|
|
119
113
|
await terminate();
|
|
120
114
|
emit('script_failed', { error: 'timeout' });
|
|
121
115
|
return { ok: false, error: 'timeout' };
|
|
122
116
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
await terminate();
|
|
133
|
-
emit('script_failed', { error: 'progress_too_large' });
|
|
134
|
-
return { ok: false, error: 'progress_too_large' };
|
|
117
|
+
if (message.type === 'call') {
|
|
118
|
+
const value = await rpc(host.call(message.command, message.args, message.options));
|
|
119
|
+
if (value && value.type === 'timeout') {
|
|
120
|
+
await terminate();
|
|
121
|
+
emit('script_failed', { error: 'timeout' });
|
|
122
|
+
return { ok: false, error: 'timeout' };
|
|
123
|
+
}
|
|
124
|
+
channel.send({ type: 'result', id: message.id, value, control: readControl() });
|
|
125
|
+
continue;
|
|
135
126
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
127
|
+
if (message.type === 'assert') {
|
|
128
|
+
const value = await rpc(host.assert(message.assertion));
|
|
129
|
+
if (value && value.type === 'timeout') {
|
|
130
|
+
await terminate();
|
|
131
|
+
emit('script_failed', { error: 'timeout' });
|
|
132
|
+
return { ok: false, error: 'timeout' };
|
|
133
|
+
}
|
|
134
|
+
channel.send({ type: 'verdict', id: message.id, value, control: readControl() });
|
|
135
|
+
continue;
|
|
144
136
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
emit('script_failed', { error: 'timeout' });
|
|
149
|
-
return { ok: false, error: 'timeout' };
|
|
137
|
+
if (message.type === 'control-point') {
|
|
138
|
+
channel.send({ type: 'control-state', id: message.id, control: readControl() });
|
|
139
|
+
continue;
|
|
150
140
|
}
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
141
|
+
if (message.type === 'progress') {
|
|
142
|
+
if (jsonBytes(message.event) > spec.policy.maxProgressBytes) {
|
|
143
|
+
await terminate();
|
|
144
|
+
emit('script_failed', { error: 'progress_too_large' });
|
|
145
|
+
return { ok: false, error: 'progress_too_large' };
|
|
146
|
+
}
|
|
147
|
+
emit('progress', message.event);
|
|
148
|
+
continue;
|
|
156
149
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
await
|
|
164
|
-
|
|
165
|
-
|
|
150
|
+
if (message.type === 'checkpoint') {
|
|
151
|
+
if (jsonBytes(message.state) > spec.policy.maxOutputBytes) {
|
|
152
|
+
await terminate();
|
|
153
|
+
emit('script_failed', { error: 'checkpoint_too_large' });
|
|
154
|
+
return { ok: false, error: 'checkpoint_too_large' };
|
|
155
|
+
}
|
|
156
|
+
const receipt = await rpc(Promise.resolve(emit('checkpoint', { name: message.name, state: message.state })));
|
|
157
|
+
if (receipt && receipt.type === 'timeout') {
|
|
158
|
+
await terminate();
|
|
159
|
+
emit('script_failed', { error: 'timeout' });
|
|
160
|
+
return { ok: false, error: 'timeout' };
|
|
161
|
+
}
|
|
162
|
+
if (receipt && receipt.ok === false) {
|
|
163
|
+
await terminate();
|
|
164
|
+
const error = receipt.error || 'checkpoint_not_persisted';
|
|
165
|
+
emit('script_failed', { error });
|
|
166
|
+
return { ok: false, error };
|
|
167
|
+
}
|
|
168
|
+
channel.send({ type: 'checkpoint-receipt', id: message.id, value: receipt, control: readControl() });
|
|
169
|
+
continue;
|
|
166
170
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
if (message.type === 'ask') {
|
|
172
|
+
const value = await rpc(agent.askAgent(message.request));
|
|
173
|
+
if (value && value.type === 'timeout') {
|
|
174
|
+
await terminate();
|
|
175
|
+
emit('script_failed', { error: 'timeout' });
|
|
176
|
+
return { ok: false, error: 'timeout' };
|
|
177
|
+
}
|
|
178
|
+
channel.send({ type: 'decision', id: message.id, value, control: readControl() });
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (message.type === 'return') {
|
|
182
|
+
if (jsonBytes(message.result) > spec.policy.maxOutputBytes) {
|
|
183
|
+
await terminate();
|
|
184
|
+
emit('script_failed', { error: 'output_too_large' });
|
|
185
|
+
return { ok: false, error: 'output_too_large' };
|
|
186
|
+
}
|
|
187
|
+
child.stdin.end();
|
|
188
|
+
const ended = await Promise.race([finished, timedOut]);
|
|
189
|
+
if (ended && ended.type === 'timeout') {
|
|
190
|
+
await terminate();
|
|
191
|
+
} else {
|
|
192
|
+
await finished;
|
|
193
|
+
}
|
|
194
|
+
emit('script_completed', { result: message.result });
|
|
195
|
+
return { ok: true, result: message.result };
|
|
196
|
+
}
|
|
197
|
+
if (message.type === 'fail') {
|
|
172
198
|
await terminate();
|
|
173
|
-
|
|
174
|
-
|
|
199
|
+
const error = message.error === 'channel_closed' || message.error === 'stopped'
|
|
200
|
+
? 'child_crashed'
|
|
201
|
+
: (message.error || 'script_failed');
|
|
202
|
+
const captured = channel.diagnostics();
|
|
203
|
+
const diagnostics = { ...captured, ...(message.diagnostic ? { error: message.diagnostic } : {}) };
|
|
204
|
+
const extra = Object.keys(diagnostics).length ? { diagnostics } : {};
|
|
205
|
+
emit('script_failed', { error, ...extra });
|
|
206
|
+
return { ok: false, error, ...extra };
|
|
175
207
|
}
|
|
176
|
-
|
|
177
|
-
const ended = await Promise.race([finished, timedOut]);
|
|
178
|
-
if (ended && ended.type === 'timeout') {
|
|
208
|
+
if (message.type === 'exit') {
|
|
179
209
|
await terminate();
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
emit('script_failed', { error: 'child_crashed' });
|
|
211
|
+
return { ok: false, error: 'child_crashed' };
|
|
182
212
|
}
|
|
183
|
-
emit('script_completed', { result: message.result });
|
|
184
|
-
return { ok: true, result: message.result };
|
|
185
|
-
}
|
|
186
|
-
if (message.type === 'fail') {
|
|
187
|
-
await terminate();
|
|
188
|
-
const error = message.error === 'channel_closed' || message.error === 'stopped'
|
|
189
|
-
? 'child_crashed'
|
|
190
|
-
: (message.error || 'script_failed');
|
|
191
|
-
emit('script_failed', { error });
|
|
192
|
-
return { ok: false, error };
|
|
193
|
-
}
|
|
194
|
-
if (message.type === 'exit') {
|
|
195
213
|
await terminate();
|
|
196
214
|
emit('script_failed', { error: 'child_crashed' });
|
|
197
215
|
return { ok: false, error: 'child_crashed' };
|
|
198
216
|
}
|
|
199
|
-
await terminate();
|
|
200
|
-
emit('script_failed', { error: 'child_crashed' });
|
|
201
|
-
return { ok: false, error: 'child_crashed' };
|
|
202
|
-
}
|
|
203
217
|
} catch {
|
|
204
218
|
await terminate();
|
|
205
219
|
emit('script_failed', { error: 'child_crashed' });
|
|
206
220
|
return { ok: false, error: 'child_crashed' };
|
|
207
221
|
} finally {
|
|
208
222
|
if (timer) clearTimeout(timer);
|
|
223
|
+
await terminate();
|
|
224
|
+
if (directory) fs.rmSync(directory, { recursive: true, force: true });
|
|
209
225
|
}
|
|
210
226
|
}
|
|
211
227
|
|
|
@@ -35,7 +35,7 @@ function pythonCandidates({ pythonPath, env = process.env } = {}) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
function probePython(executable, env) {
|
|
38
|
-
const options = { encoding: 'utf8', shell: false };
|
|
38
|
+
const options = { encoding: 'utf8', shell: false, timeout: 5000, killSignal: 'SIGKILL', maxBuffer: 16384 };
|
|
39
39
|
if (env) options.env = env;
|
|
40
40
|
const probe = spawnSync(executable, ['-c', 'import sys; print("%d.%d" % sys.version_info[:2])'], options);
|
|
41
41
|
if (probe.status !== 0 || !probe.stdout) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function boundedText(value, maxBytes) {
|
|
4
|
+
return Buffer.from(String(value)).subarray(0, maxBytes).toString('utf8').replace(/\uFFFD$/, '');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function errorDiagnostic(error) {
|
|
8
|
+
const stack = typeof error?.stack === 'string' ? error.stack : '';
|
|
9
|
+
let frameCount = 0;
|
|
10
|
+
const limitedStack = stack.split('\n').filter(line => !/^\s*at /.test(line) || ++frameCount <= 20).join('\n');
|
|
11
|
+
const frame = stack.split('\n').find(line => /:(\d+):(\d+)\)?$/.test(line));
|
|
12
|
+
const location = frame?.match(/(?:at .*?\(|at )(.*):(\d+):(\d+)\)?$/);
|
|
13
|
+
const syntaxLocation = error?.name === 'SyntaxError' ? stack.split('\n')[0].match(/^(.*):(\d+)$/) : null;
|
|
14
|
+
return { type: boundedText(error?.name || 'Error', 128), message: boundedText(error?.message ?? error, 2048),
|
|
15
|
+
...(stack ? { stack: boundedText(limitedStack, 8192), stackTruncated: frameCount > 20 || Buffer.byteLength(limitedStack) > 8192 } : {}),
|
|
16
|
+
...(syntaxLocation ? { location: { file: boundedText(syntaxLocation[1], 1024), line: Number(syntaxLocation[2]) } }
|
|
17
|
+
: location ? { location: { file: boundedText(location[1], 1024), line: Number(location[2]), column: Number(location[3]) } } : {}),
|
|
18
|
+
...(typeof error?.valuePath === 'string' ? { valuePath: boundedText(error.valuePath, 1024) } : {}) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { boundedText, errorDiagnostic };
|
|
@@ -157,6 +157,7 @@ async function restoreUnknownOperation(args = {}) {
|
|
|
157
157
|
operationId,
|
|
158
158
|
status: terminal ? checkpoint.status : 'failed',
|
|
159
159
|
error: terminal ? checkpoint.error || null : 'runtime_lost',
|
|
160
|
+
...(checkpoint.diagnostics ? { diagnostics: checkpoint.diagnostics } : {}),
|
|
160
161
|
persisted: true,
|
|
161
162
|
restored: true,
|
|
162
163
|
resumeMode: gate.ok ? 'checkpoint' : 'none',
|
package/bin/script/script-sdk.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const readline = require('node:readline');
|
|
5
|
+
const { boundedText, errorDiagnostic } = require('./script-diagnostics');
|
|
5
6
|
|
|
6
|
-
const artifactPath = process.argv[2];
|
|
7
|
+
const artifactPath = fs.realpathSync(process.argv[2]);
|
|
7
8
|
const replies = new Map();
|
|
8
9
|
const controlWaiters = [];
|
|
9
10
|
let nextId = 0;
|
|
@@ -37,6 +38,7 @@ function waitReply(id) {
|
|
|
37
38
|
rl.on('line', (line) => {
|
|
38
39
|
if (!line) return;
|
|
39
40
|
let message;
|
|
41
|
+
const parseStarted = performance.now();
|
|
40
42
|
try {
|
|
41
43
|
message = JSON.parse(line);
|
|
42
44
|
} catch {
|
|
@@ -44,10 +46,21 @@ rl.on('line', (line) => {
|
|
|
44
46
|
process.exit(1);
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
49
|
+
const inputParseMs = performance.now() - parseStarted;
|
|
47
50
|
if (message.type === 'start') {
|
|
48
51
|
applyControl(message.control);
|
|
49
|
-
run(message).catch((error) => {
|
|
50
|
-
|
|
52
|
+
run(message, inputParseMs).catch((error) => {
|
|
53
|
+
const diagnostic = errorDiagnostic(error);
|
|
54
|
+
if (message.sourceName && message.sourceName !== artifactPath) {
|
|
55
|
+
if (diagnostic.stack) {
|
|
56
|
+
const mapped = diagnostic.stack.replaceAll(artifactPath, message.sourceName);
|
|
57
|
+
diagnostic.stack = boundedText(mapped, 8192);
|
|
58
|
+
diagnostic.stackTruncated ||= Buffer.byteLength(mapped) > 8192;
|
|
59
|
+
}
|
|
60
|
+
if (diagnostic.location?.file === artifactPath) diagnostic.location.file = boundedText(message.sourceName, 1024);
|
|
61
|
+
}
|
|
62
|
+
send({ type: 'fail', error: message.extraction ? (error.code || 'extraction_failed') : diagnostic.message,
|
|
63
|
+
message: diagnostic.message, diagnostic });
|
|
51
64
|
process.exit(1);
|
|
52
65
|
});
|
|
53
66
|
return;
|
|
@@ -92,7 +105,29 @@ function holdIfPaused() {
|
|
|
92
105
|
});
|
|
93
106
|
}
|
|
94
107
|
|
|
95
|
-
async function run(start) {
|
|
108
|
+
async function run(start, inputParseMs) {
|
|
109
|
+
if (start.extraction) {
|
|
110
|
+
const executionStarted = performance.now();
|
|
111
|
+
let result;
|
|
112
|
+
if (start.extraction.mode === 'regex') result = require('../extraction/regex').extractRegex(start.inputs.response, start.extraction);
|
|
113
|
+
else {
|
|
114
|
+
const Module = require('node:module');
|
|
115
|
+
const path = require('node:path');
|
|
116
|
+
const filename = path.resolve(start.sourceName || artifactPath);
|
|
117
|
+
const loaded = new Module(filename, module);
|
|
118
|
+
loaded.filename = filename;
|
|
119
|
+
loaded.paths = Module._nodeModulePaths(path.dirname(filename));
|
|
120
|
+
loaded._compile(fs.readFileSync(artifactPath, 'utf8'), filename);
|
|
121
|
+
const main = typeof loaded.exports === 'function' ? loaded.exports : loaded.exports.main;
|
|
122
|
+
if (typeof main !== 'function') throw new TypeError('Extraction source must export main(ctx).');
|
|
123
|
+
result = await main({ inputs: start.inputs });
|
|
124
|
+
}
|
|
125
|
+
require('../extraction/json-value').validateJsonValue(result);
|
|
126
|
+
if (Buffer.byteLength(JSON.stringify(result)) > 256 * 1024) throw Object.assign(new Error('Extraction result exceeds 256 KiB.'), { code: 'extraction_output_too_large' });
|
|
127
|
+
send({ type: 'return', result, timings: { inputParseMs, executionMs: performance.now() - executionStarted } });
|
|
128
|
+
process.exit(0);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
96
131
|
const loaded = require(artifactPath);
|
|
97
132
|
const entry = start.entrypoint || 'main';
|
|
98
133
|
const main = typeof loaded === 'function' && entry === 'main' ? loaded : loaded[entry];
|