System
Edge Python reaches outside its sandbox through two system modules, net for the network and time for the clock. They ship inside Edge Python and need no imports entry, but a package can only import them when the root edge.json grants it.
import net
import timePermissions
Only the root edge.json grants. Its permissions section maps each holder to a list of entries, each module:scope.
{
"imports": { "http": "0.2.0", "analytics": "1.2.0" },
"permissions": {
"main": ["net:api.example.com", "time:wall", "time:zone"],
"analytics": ["net:api.telemetry.com", "time:monotonic"],
"http": ["net"]
}
}| Holder | Who holds its entries |
|---|---|
main | The program’s own code, every module under the root edge.json |
all | Every package, main included |
| A package name | That package only, wherever it sits in the tree |
| Entry | Allows |
|---|---|
net:<host> | request and connect to that exact host, on any port and path |
time:wall | now() |
time:monotonic | now('monotonic') |
time:zone | zone() |
A package is the nearest edge.json above its files. The root’s own modules belong to main, and a dependency answers to the name its own manifest carries. An entry without a scope, such as "net", lets the package import the module and reach nothing. What a package holds is its own entries joined with those for all, and code outside any package holds nothing. An eval group holds nothing either, whatever the manifest of the code it runs grants.
What a dependency asks for
A dependency writes what it needs in its own edge.json, in the same shape under main or all. That grants it nothing, it is the list the root has to cover. What it grants other packages counts for nothing, so trust is never inherited, and the root writes out every entry for every package it pulls in however deep. A generic client such as http asks for "net" alone and leaves the hosts to the root.
{ "name": "analytics", "permissions": { "main": ["net:api.telemetry.com", "time:monotonic"] } }edge add prints what each new package asks for. edge lock walks the whole tree, each package through its own edge.lock, and writes nothing until the root grants every ask, naming each package that misses one and the packages it came through. edge build, edge run and edge test hold the tree to the same check before anything compiles, so an edit after the last lock cannot slip past it.
error: edge.json does not grant what these packages ask for
http 0.2.0 net
analytics 1.2.0, via http 0.2.0 net:api.telemetry.com
help: grant them under "permissions" in edge.jsonA package that asks for anything needs a name, since that is what the root grants by, and no package may be named all or main, which the registry refuses to publish.
Where a grant is checked
Importing a system module the package holds no entry for fails at compile time.
'analytics' imports net, which edge.json does not grant itEvery call checks its scope again, since a url is only known once the program runs. A call outside the grant raises PermissionError, a subclass of OSError, in the coroutine that made it.
import net
try:
net.request("GET", "https://evil.example/")
except PermissionError as e:
print(e) # 'main' has no net:evil.example, edge.json grants it net:api.example.comA request or socket belongs to the package that opened it, so another package cannot read it. A malformed section stops the run before anything compiles, naming the holder and the entry.
time
| Call | Needs | Returns |
|---|---|---|
now(), now('wall') | time:wall | Nanoseconds since the Unix epoch |
now('monotonic') | time:monotonic | Nanoseconds from an arbitrary start, never going back |
zone() | time:zone | [name, offset], the IANA zone and its offset from UTC in seconds |
When no package holds a time scope the engine runs on its virtual clock. sleep and with_timeout pass at once and in order, and a call to the host takes no time, so what a program prints never depends on when it runs. Any time scope turns the wall clock on for the whole run.
net
| Call | Returns |
|---|---|
request(method, url, headers, body) | An id at once, the request goes out in the background |
response(id) | [status, headers] once the head arrived |
read(id) | The next chunk of the body, or the next message of a socket, None once it ended |
connect(url) | A socket id once the WebSocket is open |
send(id, data) | Sends bytes or a str on a socket |
close(id) | Aborts a request or closes a socket |
headers is a list of [name, value] pairs or a dict, and body is bytes, a str or None. A failure to connect or read raises OSError from the call that meets it. In a browser the request is a fetch from the Web Worker, so CORS applies on top of the grant. The CLI runs the same calls in SpiderMonkey, with its own HTTP and WebSocket clients underneath, so a program answers the same with and without --web.
import net
r = net.request("GET", "https://api.example.com/items", [["accept", "application/json"]], None)
status, headers = net.response(r)
body = b""
chunk = net.read(r)
while chunk is not None:
body += chunk
chunk = net.read(r)batch
Every system module also takes batch(calls), a list of [name, *args] lists answered in one crossing, the results in the same order. A call that waits holds the batch until every one settles, the first failure raises in the coroutine that made the batch, and each call still checks its own scope.
import net
import time
now, zone = time.batch([["now"], ["zone"]])
ids = net.batch([["request", "GET", url] for url in ["https://api.example.com/a", "https://api.example.com/b"]])
heads = net.batch([["response", i] for i in ids])Each crossing between the program and the host pays to decode the arguments and encode the answer, and a batch pays it once for all of its calls.
From a plugin
A .wasm plugin reaches the same calls through the Sys op of the ABI, with the grants of the package it sits in. A call that waits parks the program, and the plugin finishes its work in __edge_resume once the call settles, so the program sees what the plugin makes of the answer.
Embedding
A page that runs a program with createWorker passes what its root edge.json would grant beside the imports, or points baseUrl at the directory whose edge.json says it.
const worker = await createWorker({
baseUrl: new URL("./app/", location.href).href,
imports: { calc: "./calc.py" },
permissions: { main: ["net:api.example.com"] },
});Every worker lives in a sandboxed frame with an origin of its own, so a program holds none of the page’s cookies or storage and cannot reach its DOM, even one the page did not write. The page hands the frame the engine and reads the program’s files for it, and the browser lets the frame connect only to the origins its imports name and the hosts its permissions grant, so a request the grants would miss still never leaves.
See also
- Modules, edge.json and where each package’s manifest sits.
- Limits and errors, the exception tree
PermissionErrorjoins.