@qvac/harness 0.3.0 → 0.3.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/CHANGELOG.md +262 -0
- package/dist/harness.bundle +15 -11
- package/dist-lib/index.d.ts +1 -1
- package/dist-lib/lib/capabilities/completion.d.ts +5 -1
- package/dist-lib/lib/capabilities/completion.js +20 -8
- package/dist-lib/lib/helpers/tool-call-markup.js +15 -7
- package/dist-lib/lib/no-redirect-fetch-impl.d.ts +3 -2
- package/dist-lib/lib/no-redirect-fetch-impl.js +4 -1
- package/dist-lib/lib/tools/exec/adapters/adapter.d.ts +5 -1
- package/dist-lib/lib/tools/exec/adapters/python/distributions.d.ts +1 -0
- package/dist-lib/lib/tools/exec/adapters/python/distributions.js +4 -0
- package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.d.ts +72 -0
- package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.js +222 -0
- package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.d.ts +5 -0
- package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.js +29 -0
- package/dist-lib/lib/tools/exec/adapters/python/loader.js +2 -1
- package/dist-lib/lib/tools/exec/adapters/python/module-distributions.d.ts +1 -0
- package/dist-lib/lib/tools/exec/adapters/python/module-distributions.js +52 -0
- package/dist-lib/lib/tools/exec/adapters/python/named-hosts.d.ts +2 -0
- package/dist-lib/lib/tools/exec/adapters/python/named-hosts.js +15 -0
- package/dist-lib/lib/tools/exec/adapters/python/net-shim.d.ts +2 -0
- package/dist-lib/lib/tools/exec/adapters/python/net-shim.js +148 -0
- package/dist-lib/lib/tools/exec/adapters/python/output-policy.js +13 -1
- package/dist-lib/lib/tools/exec/adapters/python/pyodide.d.ts +4 -1
- package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +42 -7
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +9 -3
- package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +137 -21
- package/dist-lib/lib/tools/exec/adapters/python/run-session.d.ts +5 -0
- package/dist-lib/lib/tools/exec/adapters/python/run-session.js +33 -8
- package/dist-lib/lib/tools/exec/adapters/python/wheels.d.ts +6 -0
- package/dist-lib/lib/tools/exec/adapters/python/wheels.js +17 -3
- package/dist-lib/lib/tools/exec/adapters/python/worker/client.d.ts +2 -1
- package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +16 -3
- package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +15 -2
- package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.d.ts +6 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.js +6 -2
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.d.ts +1 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.js +8 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-node.d.ts +1 -0
- package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-node.js +8 -0
- package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.js +7 -1
- package/dist-lib/lib/tools/exec/shell-guidance.d.ts +10 -0
- package/dist-lib/lib/tools/exec/shell-guidance.js +123 -0
- package/dist-lib/lib/tools/exec/video-tools.d.ts +5 -0
- package/dist-lib/lib/tools/exec/video-tools.js +40 -0
- package/dist-lib/lib/tools/exec.d.ts +2 -1
- package/dist-lib/lib/tools/exec.js +93 -33
- package/dist-lib/lib/tools/http-request/truncate-response.d.ts +1 -0
- package/dist-lib/lib/tools/http-request/truncate-response.js +1 -1
- package/dist-lib/lib/tools/http-request.js +4 -1
- package/dist-lib/lib/tools/operations/builders.d.ts +1 -1
- package/dist-lib/lib/tools/operations/builders.js +9 -0
- package/dist-lib/lib/tools/operations/compile.js +41 -18
- package/dist-lib/lib/tools/operations/decoders.d.ts +6 -0
- package/dist-lib/lib/tools/operations/decoders.js +82 -0
- package/dist-lib/lib/tools/operations/manifest.d.ts +16 -2
- package/dist-lib/lib/tools/operations/manifest.js +6 -1
- package/dist-lib/lib/tools/skill.d.ts +3 -0
- package/dist-lib/lib/tools/skill.js +38 -5
- package/dist-lib/lib/tools/skills/manifest-parse.js +43 -1
- package/dist-lib/lib/tools/skills/manifest.d.ts +13 -0
- package/dist-lib/schema/harness.js +20 -1
- package/dist-lib/spec/hrpc/messages.js +93 -5
- package/dist-lib/spec/hyperschema/index.js +93 -5
- package/dist-lib/spec/hyperschema/schema.json +66 -1
- package/dist-lib/spec/hyperschema/types.d.ts +18 -1
- package/package.json +40 -2
- package/wheels/manifest.json +15 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare function pythonReachesNetwork(): boolean;
|
|
2
|
+
export declare const NET_SHIM = "\ndef _qvac_install_net():\n import errno, http.client, io\n import js\n from pyodide.ffi import can_run_sync, run_sync, to_js\n\n if getattr(http.client, '_qvac_net', False):\n return\n\n class _Response(http.client.HTTPResponse):\n # the body is read in full by the fetch, so there is no socket behind it\n def __init__(self, status, headers, body, url):\n self.status = self.code = status\n self.reason = http.client.responses.get(status, '')\n self.headers = self.msg = http.client.HTTPMessage()\n for name, value in headers:\n self.headers[name] = value\n self.fp = io.BytesIO(body)\n self.version = 11\n self.will_close = True\n self.chunked = False\n self.length = len(body)\n self.url = url\n self.debuglevel = 0\n\n def read(self, amt=None):\n return self.fp.read() if amt is None or amt < 0 else self.fp.read(amt)\n\n def read1(self, n=-1):\n return self.read(n)\n\n def readinto(self, b):\n data = self.fp.read(len(b))\n b[: len(data)] = data\n return len(data)\n\n def peek(self, n=0):\n pos = self.fp.tell()\n data = self.fp.read(max(n, 1))\n self.fp.seek(pos)\n return data\n\n def isclosed(self):\n return self.fp.closed\n\n def close(self):\n self.fp.close()\n\n def fileno(self):\n raise OSError('this response has no socket')\n\n class _Connection(http.client.HTTPConnection):\n scheme = 'http'\n\n def __init__(self, host, port=None, timeout=None, source_address=None, blocksize=8192, **kw):\n self.host, self.port, self.timeout = host, port, timeout\n self.sock = None\n self.debuglevel = 0\n self._tunnel_host = None\n self._request = None\n self._response = None\n\n def set_debuglevel(self, level):\n pass\n\n def set_tunnel(self, host, port=None, headers=None):\n self._tunnel_host = host\n\n def connect(self):\n pass\n\n def close(self):\n pass\n\n def putrequest(self, method, url, skip_host=False, skip_accept_encoding=False):\n self._request = [method, url, {}, b'']\n self._response = None\n\n def putheader(self, header, *values):\n self._request[2][header] = ', '.join(str(v) for v in values)\n\n # A socket connects while the request is sent, so that is where a failure lands and\n # where urllib turns it into URLError. A body sent after the headers waits for it.\n def endheaders(self, message_body=None, *, encode_chunked=False):\n if message_body is not None:\n self.send(message_body)\n if message_body is not None or self._request[0] in ('GET', 'HEAD', 'DELETE', 'OPTIONS'):\n self._response = self._fetch()\n\n def send(self, data):\n if hasattr(data, 'read'):\n data = data.read()\n if isinstance(data, str):\n data = data.encode()\n self._request[3] += bytes(data)\n\n def request(self, method, url, body=None, headers={}, *, encode_chunked=False):\n self.putrequest(method, url)\n for name, value in dict(headers).items():\n self.putheader(name, value)\n self.endheaders(body)\n\n def getresponse(self):\n response = self._response or self._fetch()\n self._response = None\n return response\n\n def _fetch(self):\n method, url, headers, body = self._request\n if not url.startswith(('http://', 'https://')):\n port = '' if self.port in (None, self.default_port) else ':%d' % self.port\n url = '%s://%s%s%s' % (self.scheme, self.host, port, url)\n if not can_run_sync():\n raise OSError(errno.EHOSTUNREACH, 'Host is unreachable')\n # the fetch hands back decoded bytes, so no encoding is asked for and none reported\n skip = ('accept-encoding', 'connection', 'host', 'content-length')\n headers = {k: v for k, v in headers.items() if k.lower() not in skip}\n headers['Accept-Encoding'] = 'identity'\n init = {'method': method, 'headers': headers, 'redirect': 'manual'}\n if body:\n init['body'] = to_js(body)\n try:\n response = run_sync(js.fetch(url, to_js(init, dict_converter=js.Object.fromEntries)))\n data = bytes(run_sync(response.arrayBuffer()).to_py())\n except Exception as error:\n # the guard's own refusal, or the transport's: either says more than an errno\n raise OSError(getattr(error, 'message', None) or str(error)) from None\n entries = [(str(k), str(v)) for k, v in response.headers.entries()]\n drop = ('content-encoding', 'transfer-encoding', 'content-length')\n kept = [(k, v) for k, v in entries if k.lower() not in drop]\n kept.append(('Content-Length', str(len(data))))\n return _Response(int(response.status), kept, data, url)\n\n class _HTTPSConnection(_Connection):\n scheme = 'https'\n default_port = 443\n\n http.client.HTTPConnection = _Connection\n http.client.HTTPSConnection = _HTTPSConnection\n http.client._qvac_net = True\n\n_qvac_install_net()\ndel _qvac_install_net\n";
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export function pythonReachesNetwork() {
|
|
2
|
+
return (typeof globalThis.Bare !== 'undefined' &&
|
|
3
|
+
typeof WebAssembly.Suspending === 'function');
|
|
4
|
+
}
|
|
5
|
+
export const NET_SHIM = `
|
|
6
|
+
def _qvac_install_net():
|
|
7
|
+
import errno, http.client, io
|
|
8
|
+
import js
|
|
9
|
+
from pyodide.ffi import can_run_sync, run_sync, to_js
|
|
10
|
+
|
|
11
|
+
if getattr(http.client, '_qvac_net', False):
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
class _Response(http.client.HTTPResponse):
|
|
15
|
+
# the body is read in full by the fetch, so there is no socket behind it
|
|
16
|
+
def __init__(self, status, headers, body, url):
|
|
17
|
+
self.status = self.code = status
|
|
18
|
+
self.reason = http.client.responses.get(status, '')
|
|
19
|
+
self.headers = self.msg = http.client.HTTPMessage()
|
|
20
|
+
for name, value in headers:
|
|
21
|
+
self.headers[name] = value
|
|
22
|
+
self.fp = io.BytesIO(body)
|
|
23
|
+
self.version = 11
|
|
24
|
+
self.will_close = True
|
|
25
|
+
self.chunked = False
|
|
26
|
+
self.length = len(body)
|
|
27
|
+
self.url = url
|
|
28
|
+
self.debuglevel = 0
|
|
29
|
+
|
|
30
|
+
def read(self, amt=None):
|
|
31
|
+
return self.fp.read() if amt is None or amt < 0 else self.fp.read(amt)
|
|
32
|
+
|
|
33
|
+
def read1(self, n=-1):
|
|
34
|
+
return self.read(n)
|
|
35
|
+
|
|
36
|
+
def readinto(self, b):
|
|
37
|
+
data = self.fp.read(len(b))
|
|
38
|
+
b[: len(data)] = data
|
|
39
|
+
return len(data)
|
|
40
|
+
|
|
41
|
+
def peek(self, n=0):
|
|
42
|
+
pos = self.fp.tell()
|
|
43
|
+
data = self.fp.read(max(n, 1))
|
|
44
|
+
self.fp.seek(pos)
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
def isclosed(self):
|
|
48
|
+
return self.fp.closed
|
|
49
|
+
|
|
50
|
+
def close(self):
|
|
51
|
+
self.fp.close()
|
|
52
|
+
|
|
53
|
+
def fileno(self):
|
|
54
|
+
raise OSError('this response has no socket')
|
|
55
|
+
|
|
56
|
+
class _Connection(http.client.HTTPConnection):
|
|
57
|
+
scheme = 'http'
|
|
58
|
+
|
|
59
|
+
def __init__(self, host, port=None, timeout=None, source_address=None, blocksize=8192, **kw):
|
|
60
|
+
self.host, self.port, self.timeout = host, port, timeout
|
|
61
|
+
self.sock = None
|
|
62
|
+
self.debuglevel = 0
|
|
63
|
+
self._tunnel_host = None
|
|
64
|
+
self._request = None
|
|
65
|
+
self._response = None
|
|
66
|
+
|
|
67
|
+
def set_debuglevel(self, level):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
def set_tunnel(self, host, port=None, headers=None):
|
|
71
|
+
self._tunnel_host = host
|
|
72
|
+
|
|
73
|
+
def connect(self):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
def close(self):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def putrequest(self, method, url, skip_host=False, skip_accept_encoding=False):
|
|
80
|
+
self._request = [method, url, {}, b'']
|
|
81
|
+
self._response = None
|
|
82
|
+
|
|
83
|
+
def putheader(self, header, *values):
|
|
84
|
+
self._request[2][header] = ', '.join(str(v) for v in values)
|
|
85
|
+
|
|
86
|
+
# A socket connects while the request is sent, so that is where a failure lands and
|
|
87
|
+
# where urllib turns it into URLError. A body sent after the headers waits for it.
|
|
88
|
+
def endheaders(self, message_body=None, *, encode_chunked=False):
|
|
89
|
+
if message_body is not None:
|
|
90
|
+
self.send(message_body)
|
|
91
|
+
if message_body is not None or self._request[0] in ('GET', 'HEAD', 'DELETE', 'OPTIONS'):
|
|
92
|
+
self._response = self._fetch()
|
|
93
|
+
|
|
94
|
+
def send(self, data):
|
|
95
|
+
if hasattr(data, 'read'):
|
|
96
|
+
data = data.read()
|
|
97
|
+
if isinstance(data, str):
|
|
98
|
+
data = data.encode()
|
|
99
|
+
self._request[3] += bytes(data)
|
|
100
|
+
|
|
101
|
+
def request(self, method, url, body=None, headers={}, *, encode_chunked=False):
|
|
102
|
+
self.putrequest(method, url)
|
|
103
|
+
for name, value in dict(headers).items():
|
|
104
|
+
self.putheader(name, value)
|
|
105
|
+
self.endheaders(body)
|
|
106
|
+
|
|
107
|
+
def getresponse(self):
|
|
108
|
+
response = self._response or self._fetch()
|
|
109
|
+
self._response = None
|
|
110
|
+
return response
|
|
111
|
+
|
|
112
|
+
def _fetch(self):
|
|
113
|
+
method, url, headers, body = self._request
|
|
114
|
+
if not url.startswith(('http://', 'https://')):
|
|
115
|
+
port = '' if self.port in (None, self.default_port) else ':%d' % self.port
|
|
116
|
+
url = '%s://%s%s%s' % (self.scheme, self.host, port, url)
|
|
117
|
+
if not can_run_sync():
|
|
118
|
+
raise OSError(errno.EHOSTUNREACH, 'Host is unreachable')
|
|
119
|
+
# the fetch hands back decoded bytes, so no encoding is asked for and none reported
|
|
120
|
+
skip = ('accept-encoding', 'connection', 'host', 'content-length')
|
|
121
|
+
headers = {k: v for k, v in headers.items() if k.lower() not in skip}
|
|
122
|
+
headers['Accept-Encoding'] = 'identity'
|
|
123
|
+
init = {'method': method, 'headers': headers, 'redirect': 'manual'}
|
|
124
|
+
if body:
|
|
125
|
+
init['body'] = to_js(body)
|
|
126
|
+
try:
|
|
127
|
+
response = run_sync(js.fetch(url, to_js(init, dict_converter=js.Object.fromEntries)))
|
|
128
|
+
data = bytes(run_sync(response.arrayBuffer()).to_py())
|
|
129
|
+
except Exception as error:
|
|
130
|
+
# the guard's own refusal, or the transport's: either says more than an errno
|
|
131
|
+
raise OSError(getattr(error, 'message', None) or str(error)) from None
|
|
132
|
+
entries = [(str(k), str(v)) for k, v in response.headers.entries()]
|
|
133
|
+
drop = ('content-encoding', 'transfer-encoding', 'content-length')
|
|
134
|
+
kept = [(k, v) for k, v in entries if k.lower() not in drop]
|
|
135
|
+
kept.append(('Content-Length', str(len(data))))
|
|
136
|
+
return _Response(int(response.status), kept, data, url)
|
|
137
|
+
|
|
138
|
+
class _HTTPSConnection(_Connection):
|
|
139
|
+
scheme = 'https'
|
|
140
|
+
default_port = 443
|
|
141
|
+
|
|
142
|
+
http.client.HTTPConnection = _Connection
|
|
143
|
+
http.client.HTTPSConnection = _HTTPSConnection
|
|
144
|
+
http.client._qvac_net = True
|
|
145
|
+
|
|
146
|
+
_qvac_install_net()
|
|
147
|
+
del _qvac_install_net
|
|
148
|
+
`;
|
|
@@ -19,7 +19,11 @@ const MIME_TYPES = {
|
|
|
19
19
|
json: 'application/json',
|
|
20
20
|
txt: 'text/plain',
|
|
21
21
|
md: 'text/markdown',
|
|
22
|
-
zip: 'application/zip'
|
|
22
|
+
zip: 'application/zip',
|
|
23
|
+
mp4: 'video/mp4',
|
|
24
|
+
webm: 'video/webm',
|
|
25
|
+
m4a: 'audio/mp4',
|
|
26
|
+
mp3: 'audio/mpeg'
|
|
23
27
|
};
|
|
24
28
|
const DENIED_EXTENSIONS = new Set([
|
|
25
29
|
'pptm',
|
|
@@ -48,6 +52,14 @@ function hasExpectedSignature(extension, bytes) {
|
|
|
48
52
|
if (extension === 'pptx' || extension === 'docx' || extension === 'xlsx' || extension === 'zip') {
|
|
49
53
|
return matches(bytes, [0x50, 0x4b, 0x03, 0x04]);
|
|
50
54
|
}
|
|
55
|
+
if (extension === 'mp4' || extension === 'm4a') {
|
|
56
|
+
return matches(bytes.slice(4), [0x66, 0x74, 0x79, 0x70]);
|
|
57
|
+
}
|
|
58
|
+
if (extension === 'webm')
|
|
59
|
+
return matches(bytes, [0x1a, 0x45, 0xdf, 0xa3]);
|
|
60
|
+
if (extension === 'mp3') {
|
|
61
|
+
return (matches(bytes, [0x49, 0x44, 0x33]) || (bytes[0] === 0xff && ((bytes[1] ?? 0) & 0xe0) === 0xe0));
|
|
62
|
+
}
|
|
51
63
|
if (extension === 'pdf')
|
|
52
64
|
return matches(bytes, [0x25, 0x50, 0x44, 0x46, 0x2d]);
|
|
53
65
|
if (extension === 'png')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AbortSignal as BareAbortSignal } from 'bare-abort-controller';
|
|
2
2
|
import { pyodideLoadOptions } from './loader.ts';
|
|
3
3
|
import { type LoadPyodideFn, type PyodideLike, type PythonRunResult } from './run-session.ts';
|
|
4
4
|
export { pyodideLoadOptions };
|
|
@@ -15,5 +15,8 @@ export type RunPythonOptions = {
|
|
|
15
15
|
readonly outputs?: readonly string[];
|
|
16
16
|
readonly workdir?: string;
|
|
17
17
|
readonly loadPyodide?: LoadPyodideFn;
|
|
18
|
+
readonly network?: boolean;
|
|
19
|
+
readonly hosts?: readonly string[];
|
|
20
|
+
readonly askConnect?: (host: string) => Promise<boolean>;
|
|
18
21
|
};
|
|
19
22
|
export declare function runPython(code: string, options?: RunPythonOptions): Promise<PythonRunResult>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { AbortController } from 'bare-abort-controller';
|
|
1
2
|
import process from 'process';
|
|
2
3
|
import ScopeLock from 'scope-lock';
|
|
3
4
|
import { AbortedError, raceAbort } from '../../../../helpers/race-abort.js';
|
|
4
5
|
import { hlog } from '../../../../log.js';
|
|
5
6
|
import { MAX_OUTPUT_CHARS } from '../../runner.js';
|
|
7
|
+
import { startInterruptTimer } from './interrupt-timer.js';
|
|
6
8
|
import { defaultLoad, pyodideLoadOptions } from './loader.js';
|
|
7
9
|
import { isPyodideFatal, performRun } from './run-session.js';
|
|
8
10
|
import { runPythonInWorker } from './worker/client.js';
|
|
@@ -32,7 +34,11 @@ function getPyodide(load) {
|
|
|
32
34
|
}
|
|
33
35
|
const runLock = new ScopeLock();
|
|
34
36
|
async function runInProcess(load, spec, options) {
|
|
37
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
38
|
+
const timeoutMessage = `Command timed out after ${options.timeoutMs}ms`;
|
|
35
39
|
let timedOut = false;
|
|
40
|
+
const stop = new AbortController();
|
|
41
|
+
const ask = options.askConnect;
|
|
36
42
|
const run = (async () => {
|
|
37
43
|
await runLock.lock();
|
|
38
44
|
try {
|
|
@@ -40,12 +46,26 @@ async function runInProcess(load, spec, options) {
|
|
|
40
46
|
if (isCancelled())
|
|
41
47
|
throw new AbortedError();
|
|
42
48
|
const py = await getPyodide(load);
|
|
43
|
-
const
|
|
44
|
-
|
|
49
|
+
const timer = startInterruptTimer(Math.max(0, deadline - Date.now()));
|
|
50
|
+
py.setInterruptBuffer(timer.interrupt);
|
|
51
|
+
let outcome;
|
|
52
|
+
try {
|
|
53
|
+
outcome = await performRun(py, spec, {
|
|
54
|
+
isCancelled,
|
|
55
|
+
askConnect: ask && ((host) => raceAbort(ask(host), stop.signal).catch(() => false))
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (timer.disarm())
|
|
60
|
+
timedOut = true;
|
|
61
|
+
}
|
|
62
|
+
if (outcome.evict) {
|
|
45
63
|
cache.delete(load);
|
|
46
64
|
hlog('python: interpreter evicted; next run boots fresh');
|
|
47
65
|
}
|
|
48
|
-
|
|
66
|
+
if (timedOut)
|
|
67
|
+
throw new Error(timeoutMessage);
|
|
68
|
+
return outcome.result;
|
|
49
69
|
}
|
|
50
70
|
catch (error) {
|
|
51
71
|
if (isPyodideFatal(error))
|
|
@@ -60,7 +80,7 @@ async function runInProcess(load, spec, options) {
|
|
|
60
80
|
const timeout = new Promise((_, reject) => {
|
|
61
81
|
timer = setTimeout(() => {
|
|
62
82
|
timedOut = true;
|
|
63
|
-
reject(new Error(
|
|
83
|
+
reject(new Error(timeoutMessage));
|
|
64
84
|
}, options.timeoutMs);
|
|
65
85
|
});
|
|
66
86
|
try {
|
|
@@ -69,6 +89,7 @@ async function runInProcess(load, spec, options) {
|
|
|
69
89
|
finally {
|
|
70
90
|
if (timer)
|
|
71
91
|
clearTimeout(timer);
|
|
92
|
+
stop.abort(undefined);
|
|
72
93
|
}
|
|
73
94
|
}
|
|
74
95
|
function workerAvailable() {
|
|
@@ -88,20 +109,34 @@ export async function runPython(code, options = {}) {
|
|
|
88
109
|
outputs: options.outputs,
|
|
89
110
|
workdir: options.workdir,
|
|
90
111
|
maxOutputChars: options.maxOutputChars ?? MAX_OUTPUT_CHARS,
|
|
112
|
+
network: options.network === true,
|
|
113
|
+
hosts: options.hosts,
|
|
91
114
|
installTimeoutMs: timeoutMs
|
|
92
115
|
};
|
|
93
116
|
if (options.loadPyodide) {
|
|
94
|
-
return runInProcess(options.loadPyodide, spec, {
|
|
117
|
+
return runInProcess(options.loadPyodide, spec, {
|
|
118
|
+
timeoutMs,
|
|
119
|
+
signal: options.signal,
|
|
120
|
+
askConnect: options.askConnect
|
|
121
|
+
});
|
|
95
122
|
}
|
|
96
123
|
if (!workerAvailable()) {
|
|
97
124
|
hlog('python: worker entry unavailable in this bundle; running in-process');
|
|
98
|
-
return runInProcess(defaultLoad, spec, {
|
|
125
|
+
return runInProcess(defaultLoad, spec, {
|
|
126
|
+
timeoutMs,
|
|
127
|
+
signal: options.signal,
|
|
128
|
+
askConnect: options.askConnect
|
|
129
|
+
});
|
|
99
130
|
}
|
|
100
131
|
await runLock.lock();
|
|
101
132
|
try {
|
|
102
133
|
if (options.signal?.aborted)
|
|
103
134
|
throw new AbortedError();
|
|
104
|
-
return await runPythonInWorker(spec, {
|
|
135
|
+
return await runPythonInWorker(spec, {
|
|
136
|
+
timeoutMs,
|
|
137
|
+
signal: options.signal,
|
|
138
|
+
askConnect: options.askConnect
|
|
139
|
+
});
|
|
105
140
|
}
|
|
106
141
|
finally {
|
|
107
142
|
runLock.unlock();
|
|
@@ -4,8 +4,11 @@ import { type LoadPyodideFn } from './pyodide.ts';
|
|
|
4
4
|
export declare function isPythonLanguage(language: unknown): boolean;
|
|
5
5
|
export declare function runsThroughPyodide(language: unknown, disablePyodide: boolean): boolean;
|
|
6
6
|
export declare function unwrapPythonSource(command: string): string | null;
|
|
7
|
+
export declare function withoutProgramDir(command: string): string;
|
|
7
8
|
export declare function pipInstallTargets(command: string): string[] | null;
|
|
8
|
-
export declare function
|
|
9
|
+
export declare function pinned(spec: string): string;
|
|
10
|
+
export declare function pipMessage(targets: readonly string[] | null, sandbox?: boolean): string;
|
|
11
|
+
export declare function pipFromSourceHint(stderr: string, source: string, packages: readonly string[] | undefined, sandbox?: boolean): string | null;
|
|
9
12
|
export declare function stringLiteralProgramHint(source: string): string | null;
|
|
10
13
|
export declare function checkCollapsedCompound(source: string): string | null;
|
|
11
14
|
export declare function mangledPptxApiHint(source: string): string | null;
|
|
@@ -44,7 +47,7 @@ export declare function parseOutputs(value: unknown): string[] | {
|
|
|
44
47
|
export declare function parseWorkdir(value: unknown): string | {
|
|
45
48
|
error: string;
|
|
46
49
|
} | undefined;
|
|
47
|
-
export declare function missingModuleHint(stderr: string, packages: readonly string[] | undefined): string | null;
|
|
50
|
+
export declare function missingModuleHint(stderr: string, packages: readonly string[] | undefined, sandbox?: boolean, network?: boolean): string | null;
|
|
48
51
|
export declare function nonePlaceholderHint(stderr: string): string | null;
|
|
49
52
|
export declare function slideShapesHint(stderr: string): string | null;
|
|
50
53
|
export declare function shapeFontHint(stderr: string): string | null;
|
|
@@ -53,6 +56,9 @@ export declare function paragraphFillHint(stderr: string): string | null;
|
|
|
53
56
|
export declare function readOnlyPropertyHint(stderr: string): string | null;
|
|
54
57
|
export declare function paragraphIndexHint(stderr: string, outputs: readonly string[]): string | null;
|
|
55
58
|
export declare function shellNameHint(stderr: string): string | null;
|
|
59
|
+
export declare function noProcessHint(stderr: string, sandbox?: boolean): string | null;
|
|
60
|
+
export declare function noNetworkHint(stderr: string, tools?: readonly string[], network?: boolean): string | null;
|
|
61
|
+
export declare function nativeWheelHint(stderr: string): string | null;
|
|
56
62
|
export declare function packageFetchFailedHint(stderr: string): string | null;
|
|
57
63
|
export declare const IMAGE_INPUT: RegExp;
|
|
58
64
|
export declare function pptxHasEmbeddedMedia(bytes: Uint8Array): boolean;
|
|
@@ -72,4 +78,4 @@ export declare function checkTurnImagesStaged(source: string, inputs: readonly E
|
|
|
72
78
|
export declare function deckDeliveredStop(attachments: readonly {
|
|
73
79
|
readonly fileName: string;
|
|
74
80
|
}[] | undefined, exitCode: number, source?: string): string | null;
|
|
75
|
-
export declare function createPythonAdapter(disabled: boolean, loadPyodide?: LoadPyodideFn): ExecutionAdapter;
|
|
81
|
+
export declare function createPythonAdapter(disabled: boolean, loadPyodide?: LoadPyodideFn, network?: boolean): ExecutionAdapter;
|
|
@@ -2,6 +2,10 @@ import b4a from 'b4a';
|
|
|
2
2
|
import { attachOutputs, stageInputs } from './attachments.js';
|
|
3
3
|
import { checkFileName, checkOutputName, MAX_INPUTS, MAX_OUTPUTS } from './output-policy.js';
|
|
4
4
|
import { runPython } from './pyodide.js';
|
|
5
|
+
import { isTranscoder, TRANSCODE_ERROR, VIDEO_TOOLS_ERROR, videoTool } from '../../video-tools.js';
|
|
6
|
+
import { moduleDistribution } from './distributions.js';
|
|
7
|
+
import { namedHosts } from './named-hosts.js';
|
|
8
|
+
import { normalizeDistribution, planVendoredInstall, vendoredSpec } from './wheels.js';
|
|
5
9
|
export function isPythonLanguage(language) {
|
|
6
10
|
return typeof language === 'string' && language.trim().toLowerCase() === 'python';
|
|
7
11
|
}
|
|
@@ -114,7 +118,7 @@ function unwrapBareInterpreter(s) {
|
|
|
114
118
|
return body || null;
|
|
115
119
|
}
|
|
116
120
|
export function unwrapPythonSource(command) {
|
|
117
|
-
const trimmed = command.trim();
|
|
121
|
+
const trimmed = withoutProgramDir(command).trim();
|
|
118
122
|
const heredoc = unwrapHeredoc(trimmed);
|
|
119
123
|
if (heredoc !== null)
|
|
120
124
|
return heredoc;
|
|
@@ -123,8 +127,11 @@ export function unwrapPythonSource(command) {
|
|
|
123
127
|
return dashC;
|
|
124
128
|
return unwrapBareInterpreter(trimmed);
|
|
125
129
|
}
|
|
130
|
+
export function withoutProgramDir(command) {
|
|
131
|
+
return command.trimStart().replace(/^[^\s'"]*[/\\](?=[^\s/\\]+(\s|$))/, '');
|
|
132
|
+
}
|
|
126
133
|
function pipInstallRest(command) {
|
|
127
|
-
const s = command.trim();
|
|
134
|
+
const s = withoutProgramDir(command).trim();
|
|
128
135
|
let i = 0;
|
|
129
136
|
const pip = leadingToken(s, PIPS);
|
|
130
137
|
if (pip) {
|
|
@@ -178,17 +185,64 @@ export function pipInstallTargets(command) {
|
|
|
178
185
|
continue;
|
|
179
186
|
if (!REQUIREMENT.test(token))
|
|
180
187
|
return null;
|
|
188
|
+
if (/^pip3?$/i.test(token.split(/[=<>!~[]/)[0]))
|
|
189
|
+
continue;
|
|
181
190
|
targets.push(token);
|
|
182
191
|
}
|
|
183
192
|
return targets.length > 0 ? targets : null;
|
|
184
193
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
194
|
+
const SANDBOX_NO_INSTALL = 'and sandbox mode installs only the packages bundled with this app, which do not include it. Use only what is already installed, or tell the user it is unavailable while sandbox mode is on.';
|
|
195
|
+
const specName = (spec) => normalizeDistribution(spec.split(/[=<>!~[]/)[0]);
|
|
196
|
+
export function pinned(spec) {
|
|
197
|
+
return /[=<>!~]/.test(spec) ? spec : (vendoredSpec(spec) ?? spec);
|
|
198
|
+
}
|
|
199
|
+
export function pipMessage(targets, sandbox = false) {
|
|
200
|
+
const specs = targets?.map(pinned) ?? null;
|
|
201
|
+
if (sandbox && !(specs?.length && planVendoredInstall(specs))) {
|
|
202
|
+
return `pip is not available, ${SANDBOX_NO_INSTALL}`;
|
|
203
|
+
}
|
|
204
|
+
const declare = specs ? `packages: ${JSON.stringify(specs)}` : 'the PyPI names in packages';
|
|
205
|
+
return `pip is not available. Call exec with language "python", ${declare} and the Python source in command. Installing and running are the same one call.`;
|
|
206
|
+
}
|
|
207
|
+
const PIP_CALL = /\b(?:subprocess\.\w+|os\.(?:system|popen)|Popen|check_(?:output|call))\s*\(/;
|
|
208
|
+
const STRING_LITERAL = /"([^"\n]*)"|'([^'\n]*)'/g;
|
|
209
|
+
function pipLineTargets(line) {
|
|
210
|
+
const words = [...line.matchAll(STRING_LITERAL)].map((match) => (match[1] ?? match[2]));
|
|
211
|
+
const whole = words.find((word) => /\bpip3?\b[^]*\binstall\b/.test(word));
|
|
212
|
+
if (whole !== undefined)
|
|
213
|
+
return pipInstallTargets(whole.trim());
|
|
214
|
+
const at = words.findIndex((word) => /^pip3?$/.test(word));
|
|
215
|
+
if (at === -1)
|
|
216
|
+
return undefined;
|
|
217
|
+
return pipInstallTargets(words.slice(at).join(' '));
|
|
218
|
+
}
|
|
219
|
+
export function pipFromSourceHint(stderr, source, packages, sandbox = false) {
|
|
220
|
+
if (!/emscripten does not support processes/.test(stderr))
|
|
221
|
+
return null;
|
|
222
|
+
for (const line of source.split('\n')) {
|
|
223
|
+
if (!PIP_CALL.test(line))
|
|
224
|
+
continue;
|
|
225
|
+
const targets = pipLineTargets(line);
|
|
226
|
+
if (targets === undefined)
|
|
227
|
+
continue;
|
|
228
|
+
const declared = packages ?? [];
|
|
229
|
+
const has = new Set(declared.map(specName));
|
|
230
|
+
const missing = (targets ?? []).filter((t) => !has.has(specName(t))).map(pinned);
|
|
231
|
+
if (sandbox && (targets === null || !planVendoredInstall([...declared, ...missing]))) {
|
|
232
|
+
return `pip cannot run here: this Python cannot start processes, so that line failed, ${SANDBOX_NO_INSTALL} Delete the pip line.`;
|
|
233
|
+
}
|
|
234
|
+
if (targets === null) {
|
|
235
|
+
return 'pip cannot run here: this Python cannot start processes, so that line failed. Delete it and declare the PyPI names in packages on this same call.';
|
|
236
|
+
}
|
|
237
|
+
if (missing.length) {
|
|
238
|
+
return `pip cannot run here: this Python cannot start processes, so that line failed. Delete it and send the same source with packages: ${JSON.stringify([...declared, ...missing])} on this one call. Installing and running are the same call.`;
|
|
239
|
+
}
|
|
240
|
+
if (declared.length) {
|
|
241
|
+
return `pip cannot run here: this Python cannot start processes, so that line failed. packages: ${JSON.stringify(declared)} already installs before your code runs, so delete the pip line and send the rest.`;
|
|
242
|
+
}
|
|
243
|
+
return 'pip cannot run here: this Python cannot start processes, so that line failed. Delete it and declare the PyPI names in packages on this same call.';
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
192
246
|
}
|
|
193
247
|
export function stringLiteralProgramHint(source) {
|
|
194
248
|
const first = source.trimStart()[0];
|
|
@@ -771,16 +825,35 @@ function prepare(args, cwd) {
|
|
|
771
825
|
return workdir;
|
|
772
826
|
return { packages, inputs, outputs, workdir };
|
|
773
827
|
}
|
|
774
|
-
export function missingModuleHint(stderr, packages) {
|
|
828
|
+
export function missingModuleHint(stderr, packages, sandbox = false, network = false) {
|
|
775
829
|
const module = /ModuleNotFoundError: No module named '([^']+)'/.exec(stderr)?.[1];
|
|
776
830
|
if (!module)
|
|
777
831
|
return null;
|
|
778
832
|
const root = module.split('.')[0];
|
|
779
|
-
const
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
833
|
+
const tool = videoTool(root);
|
|
834
|
+
if (tool && (!network || isTranscoder(tool)))
|
|
835
|
+
return network ? TRANSCODE_ERROR : VIDEO_TOOLS_ERROR;
|
|
836
|
+
if (module !== root) {
|
|
837
|
+
const then = sandbox
|
|
838
|
+
? 'If the path is right it ships as its own distribution under a shared namespace, which sandbox mode cannot install: tell the user.'
|
|
839
|
+
: 'If the path is right it ships as its own distribution under a shared namespace, as google.* does: declare that distribution in packages.';
|
|
840
|
+
return `"${module}" failed to import although "${root}" loaded. Check the import path first. ${then}`;
|
|
841
|
+
}
|
|
842
|
+
const distribution = moduleDistribution(root);
|
|
843
|
+
const provider = distribution ?? root;
|
|
844
|
+
const vendored = vendoredSpec(provider);
|
|
845
|
+
if (sandbox && !vendored)
|
|
846
|
+
return `module "${root}" is not installed, ${SANDBOX_NO_INSTALL}`;
|
|
847
|
+
const spec = vendored ?? provider;
|
|
848
|
+
const renamed = distribution !== null && distribution.toLowerCase() !== root.toLowerCase();
|
|
849
|
+
const declared = (packages ?? []).map(specName);
|
|
850
|
+
if (declared.includes(root.toLowerCase()) && renamed) {
|
|
851
|
+
return `"${root}" was declared in packages but does not provide that module; the distribution that does is "${spec}": put that in packages instead. There is no pip.`;
|
|
852
|
+
}
|
|
853
|
+
if (declared.includes(root.toLowerCase()) || declared.includes(provider.toLowerCase())) {
|
|
854
|
+
return `"${provider}" is already declared in packages and still does not provide "${root}", so declaring it again cannot help. Import the module name the library really exports, or tell the user it is unavailable on this device.`;
|
|
855
|
+
}
|
|
856
|
+
return `module "${root}" is not installed: add "${spec}" to packages (packages: ["${spec}"]) and run the same source again. There is no pip.`;
|
|
784
857
|
}
|
|
785
858
|
export function nonePlaceholderHint(stderr) {
|
|
786
859
|
if (!/AttributeError: 'NoneType' object has no attribute '(text|text_frame)'/.test(stderr)) {
|
|
@@ -833,6 +906,33 @@ export function shellNameHint(stderr) {
|
|
|
833
906
|
return null;
|
|
834
907
|
return `"${name}" is not defined because command already runs as Python source — remove the ${name} invocation and pass only the Python code itself`;
|
|
835
908
|
}
|
|
909
|
+
export function noProcessHint(stderr, sandbox = false) {
|
|
910
|
+
if (!/emscripten does not support processes/.test(stderr))
|
|
911
|
+
return null;
|
|
912
|
+
const packages = sandbox
|
|
913
|
+
? ' In sandbox mode only the packages bundled with this app install;'
|
|
914
|
+
: ' Declare PyPI packages in packages;';
|
|
915
|
+
return `Python here cannot start processes: there is no shell and no pip from the source.${packages} run a shell command as its own exec call without language.`;
|
|
916
|
+
}
|
|
917
|
+
const FETCH_TOOLS = ['http_request', 'web_fetch'];
|
|
918
|
+
export function noNetworkHint(stderr, tools, network = false) {
|
|
919
|
+
if (network)
|
|
920
|
+
return null;
|
|
921
|
+
if (!/Host is unreachable|Network is unreachable|socket\.gaierror|URLError|Temporary failure in name resolution|NewConnectionError|XMLHttpRequest is not defined/.test(stderr)) {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
const failed = 'Python here cannot open network connections (requests, urllib and sockets fail), and retrying will not change that.';
|
|
925
|
+
const fetchers = FETCH_TOOLS.filter((name) => !tools || tools.includes(name));
|
|
926
|
+
return fetchers.length
|
|
927
|
+
? `${failed} Fetch the URL with the ${fetchers.join(' or ')} tool and pass the data into the source.`
|
|
928
|
+
: `${failed} No tool that fetches a URL is available to you, so tell the user.`;
|
|
929
|
+
}
|
|
930
|
+
export function nativeWheelHint(stderr) {
|
|
931
|
+
const name = /Can't find a pure Python 3 wheel for '([^']+)'/.exec(stderr)?.[1];
|
|
932
|
+
if (!name)
|
|
933
|
+
return null;
|
|
934
|
+
return `"${name}" is built from compiled code, and this device installs only pure-Python packages plus the set already built into its Python. Nothing about this call can install it: use a pure-Python library instead, or tell the user it is not available here.`;
|
|
935
|
+
}
|
|
836
936
|
export function packageFetchFailedHint(stderr) {
|
|
837
937
|
const name = /Can't fetch metadata for '([^']+)'/.exec(stderr)?.[1];
|
|
838
938
|
if (!name)
|
|
@@ -964,7 +1064,7 @@ async function collect(result, declared, attach, runBudget, source) {
|
|
|
964
1064
|
}
|
|
965
1065
|
}
|
|
966
1066
|
}
|
|
967
|
-
const { attachments, missingOutputs, rejectedOutputs } = await attachOutputs(attach, result.files
|
|
1067
|
+
const { attachments, missingOutputs, rejectedOutputs } = await attachOutputs(attach, (result.exitCode === 0 && result.files) || new Map(), declared, runBudget);
|
|
968
1068
|
const rejected = [...(result.rejected ?? []), ...rejectedOutputs];
|
|
969
1069
|
const stop = deckDeliveredStop(attachments, result.exitCode, source);
|
|
970
1070
|
if (stop && result.exitCode === 0) {
|
|
@@ -987,7 +1087,7 @@ async function collect(result, declared, attach, runBudget, source) {
|
|
|
987
1087
|
...(rejected.length > 0 && { rejectedOutputs: rejected })
|
|
988
1088
|
};
|
|
989
1089
|
}
|
|
990
|
-
export function createPythonAdapter(disabled, loadPyodide) {
|
|
1090
|
+
export function createPythonAdapter(disabled, loadPyodide, network = false) {
|
|
991
1091
|
return {
|
|
992
1092
|
resolve(args, ctx) {
|
|
993
1093
|
if (!runsThroughPyodide(args.language, disabled))
|
|
@@ -997,6 +1097,12 @@ export function createPythonAdapter(disabled, loadPyodide) {
|
|
|
997
1097
|
const fetchAttachment = ctx?.fetchAttachment;
|
|
998
1098
|
const turnImages = ctx?.turnImages;
|
|
999
1099
|
const runBudget = ctx?.runBudget;
|
|
1100
|
+
const sandbox = ctx?.sandboxMode === true;
|
|
1101
|
+
const toolNames = ctx?.toolNames;
|
|
1102
|
+
const approved = ctx?.approval === 'allowed' || ctx?.approval === 'skip-all';
|
|
1103
|
+
const reach = network && !sandbox && approved;
|
|
1104
|
+
const gated = reach && ctx?.approval === 'allowed';
|
|
1105
|
+
const confirmConnect = ctx?.confirmConnect;
|
|
1000
1106
|
return {
|
|
1001
1107
|
async run(options) {
|
|
1002
1108
|
if (!command)
|
|
@@ -1035,10 +1141,13 @@ export function createPythonAdapter(disabled, loadPyodide) {
|
|
|
1035
1141
|
});
|
|
1036
1142
|
if (staged && 'error' in staged)
|
|
1037
1143
|
return fail(staged.error);
|
|
1038
|
-
const install = pipInstallTargets(command);
|
|
1144
|
+
const install = pipInstallTargets(command)?.map(pinned) ?? null;
|
|
1039
1145
|
const packages = install
|
|
1040
1146
|
? [...new Set([...(prepared.packages ?? []), ...install])]
|
|
1041
1147
|
: prepared.packages;
|
|
1148
|
+
if (sandbox && install && !planVendoredInstall(packages ?? [])) {
|
|
1149
|
+
return fail(pipMessage(install, true));
|
|
1150
|
+
}
|
|
1042
1151
|
const result = await runPython(install ? 'pass' : source, {
|
|
1043
1152
|
timeoutMs: options.timeoutMs,
|
|
1044
1153
|
maxOutputChars: options.maxOutputChars,
|
|
@@ -1047,7 +1156,10 @@ export function createPythonAdapter(disabled, loadPyodide) {
|
|
|
1047
1156
|
inputs: staged?.files,
|
|
1048
1157
|
outputs: prepared.outputs,
|
|
1049
1158
|
workdir: prepared.workdir,
|
|
1050
|
-
loadPyodide
|
|
1159
|
+
loadPyodide,
|
|
1160
|
+
network: reach,
|
|
1161
|
+
hosts: gated ? namedHosts(source) : undefined,
|
|
1162
|
+
askConnect: gated ? confirmConnect : undefined
|
|
1051
1163
|
});
|
|
1052
1164
|
const soft = softFailedDeckHint(result.stdout, result.exitCode, source);
|
|
1053
1165
|
if (soft)
|
|
@@ -1056,7 +1168,11 @@ export function createPythonAdapter(disabled, loadPyodide) {
|
|
|
1056
1168
|
if (missingPic)
|
|
1057
1169
|
return fail(missingPic);
|
|
1058
1170
|
const hint = packageFetchFailedHint(result.stderr) ??
|
|
1059
|
-
|
|
1171
|
+
nativeWheelHint(result.stderr) ??
|
|
1172
|
+
missingModuleHint(result.stderr, packages, sandbox, reach) ??
|
|
1173
|
+
pipFromSourceHint(result.stderr, source, packages, sandbox) ??
|
|
1174
|
+
noProcessHint(result.stderr, sandbox) ??
|
|
1175
|
+
noNetworkHint(result.stderr, toolNames, reach) ??
|
|
1060
1176
|
missingFileHint(result.stderr) ??
|
|
1061
1177
|
rgbColorImportHint(result.stderr) ??
|
|
1062
1178
|
nonePlaceholderHint(result.stderr) ??
|
|
@@ -1077,7 +1193,7 @@ export function createPythonAdapter(disabled, loadPyodide) {
|
|
|
1077
1193
|
const installed = install && result.exitCode === 0
|
|
1078
1194
|
? {
|
|
1079
1195
|
...hinted,
|
|
1080
|
-
stdout: `installed: ${packages?.join(', ')}\nnothing was run —
|
|
1196
|
+
stdout: `installed: ${packages?.join(', ')}\nnothing was run — call exec again with language "python", packages: ${JSON.stringify(packages ?? [])} and the Python source in command\n${hinted.stdout}`
|
|
1081
1197
|
}
|
|
1082
1198
|
: hinted;
|
|
1083
1199
|
const undelivered = result.undeclared?.length
|
|
@@ -4,6 +4,8 @@ export type PyodideLike = {
|
|
|
4
4
|
filename?: string;
|
|
5
5
|
}): unknown;
|
|
6
6
|
runPythonAsync(code: string): Promise<unknown>;
|
|
7
|
+
ffi?: unknown;
|
|
8
|
+
pyimport?(name: string): unknown;
|
|
7
9
|
loadPackage(name: string): Promise<unknown>;
|
|
8
10
|
setStdout(options: {
|
|
9
11
|
batched: (output: string) => void;
|
|
@@ -62,6 +64,8 @@ export type RunSpec = {
|
|
|
62
64
|
readonly workdir?: string | undefined;
|
|
63
65
|
readonly maxOutputChars: number;
|
|
64
66
|
readonly installTimeoutMs: number;
|
|
67
|
+
readonly network?: boolean | undefined;
|
|
68
|
+
readonly hosts?: readonly string[] | undefined;
|
|
65
69
|
};
|
|
66
70
|
export type RunOutcome = {
|
|
67
71
|
readonly result: PythonRunResult;
|
|
@@ -72,4 +76,5 @@ export declare function isPyodideFatal(error: unknown): boolean;
|
|
|
72
76
|
export declare function repairDoubleEscapedSource(code: string, message: string): string | null;
|
|
73
77
|
export declare function performRun(py: PyodideLike, spec: RunSpec, opts?: {
|
|
74
78
|
readonly isCancelled?: () => boolean;
|
|
79
|
+
readonly askConnect?: ((host: string) => Promise<boolean>) | undefined;
|
|
75
80
|
}): Promise<RunOutcome>;
|