Edge Python
Esc

Go to

Commands

Sign In

Sign in to publish packages and pin them by sha256.


By continuing, you accept our Terms and Conditions.

@
Palette

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 time
python

Permissions

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"]
  }
}
json
HolderWho holds its entries
mainThe program’s own code, every module under the root edge.json
allEvery package, main included
A package nameThat package only, wherever it sits in the tree
EntryAllows
net:<host>request and connect to that exact host, on any port and path
time:wallnow()
time:monotonicnow('monotonic')
time:zonezone()

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"] } }
json

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.json
text

A 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 it
text

Every 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.com
python

A 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

CallNeedsReturns
now(), now('wall')time:wallNanoseconds since the Unix epoch
now('monotonic')time:monotonicNanoseconds 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

CallReturns
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)
python

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])
python

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"] },
});
js

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 PermissionError joins.