Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,15 @@ states both plainly rather than counting them as defenses.
- **Sandboxed execution.** WebVM uses CheerpX. Notebook and headless script
execution use sealed workers. Apps use opaque-origin sandboxed iframes and
currently run only on Chrome. Store and web builds refuse direct remote
JavaScript imports without requesting the module source. Preview permits
audited literal static remote imports. Dynamic imports are refused in every
package.
Pins are optional there, imported code inherits the run's capabilities, and
visible Notebook output is not yet fenced before its actor reads it. The
JavaScript imports without requesting the module source. Preview targets may
permit audited literal static remote imports where the browser loader supports
them. Dynamic imports are refused in every package.
Any remote module restricts the whole run to compute only. Runtime network
and file access, agents, model calls, browser and site access, and dweb are
disabled by both worker shims and host relay checks. A remote module cannot
import a local toolbox module. Returned values,
console output, and errors are fenced as untrusted. Optional pins verify the
bytes for reproducibility but do not make the code trusted. The
sealed worker protects the extension process. It does not make remote code a
trusted dependency. The broader network-derived code contract for Store is
tracked separately.
Expand Down
21 changes: 14 additions & 7 deletions docs/design/js-superpower/03-module-imports-via-egress.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

Remote module imports are a package policy, not a worker capability.
`REMOTE_MODULE_IMPORTS_ENABLED` is generated with the channel configuration.
Preview packages enable it. Store and web packages disable it.
Supported Preview targets enable it. Store, web, and Firefox packages disable it.

## Supported imports

Notebook and Script entry code can use literal static imports and re-exports.
Relative files, `peerd:std`, `peerd:wasi`, and `peerd:toolbox/<name>` resolve
through the host. Preview also accepts literal static HTTP and HTTPS imports.
through the host where the browser loader supports them. Chrome Preview also
accepts literal static HTTP and HTTPS imports.

Dynamic imports, `peerd.self.import`, computed specifiers, import attributes,
other URL schemes, and extension-absolute paths are refused with a stable
Expand Down Expand Up @@ -43,12 +44,18 @@ imports before storage. For Preview remote imports, it checks specifier policy
and the direct graph-count limit without fetching third-party source. Remote
availability, source syntax, and transitive dependencies are runtime checks.

## Remaining trust work
## Trust boundary

The headless Script host marks remote module use as egress and fences its
result. The visible Notebook host still needs to carry equivalent provenance
into `js_notebook` output. The broader policy for executing code that was
fetched as ordinary data is tracked separately.
Both execution hosts mark the whole resolved graph as remote-derived when any
HTTPS module is present. The worker profile removes network, file, agent,
provider, browser, site, and dweb clients. Host relays refuse forged requests.
A remote module cannot import a local toolbox module. Returned values, logs,
and errors cross the untrusted-content fence before reaching the model.

Firefox refuses remote imports before fetching because its worker loader cannot
run the generated child module URLs. Native Firefox loading for local and
remote static graphs is tracked separately. The broader policy for executing
code fetched as ordinary data is also tracked separately.

## Authoritative sources

Expand Down
10 changes: 6 additions & 4 deletions docs/security/RED-TEAM-RESULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

_Generated from the current checkout by the command above._

13 of 13 scenarios held. 199 of 199 individual hostile probes blocked.
13 of 13 scenarios held. 201 of 201 individual hostile probes blocked.

| # | Attack | Adversary | Asset | Invariant | Result |
|---|--------|-----------|-------|-----------|--------|
Expand Down Expand Up @@ -144,10 +144,10 @@ _Generated from the current checkout by the command above._

- Adversary: malicious sandboxed code
- Asset: the host origin, the network, and other sandbox instances
- Claim checked: Across all three sandbox kinds, confinement holds: the Notebook realm exposes only the audited fetch bridge (raw channels throw, native fetch unrecoverable, bridge un-unseatable) and no same-origin durable store; the Cache API and IndexedDB both throw, so the sealed extension-origin worker cannot reach the `peerd` database; an App cannot break out of its iframe or impersonate the service worker to issue actor commands; and the WebVM HTTP bridge refuses non-http(s) schemes, scrubs CRLF header injection, drops any smuggled auth field, and confirms body-bearing verbs.
- Claim checked: Across all three sandbox kinds, confinement holds: the Notebook realm exposes only the audited fetch bridge (raw channels throw, native fetch unrecoverable, bridge un-unseatable) and no same-origin durable store; the Cache API and IndexedDB both throw, so the sealed extension-origin worker cannot reach the `peerd` database; a remote module restricts its whole run to compute only and all remote-controlled output is fenced; an App cannot break out of its iframe or impersonate the service worker to issue actor commands; and the WebVM HTTP bridge refuses non-http(s) schemes, scrubs CRLF header injection, drops any smuggled auth field, and confirms body-bearing verbs.
- Threat-model invariant: INV-6
- Defenses exercised: applyRealmSeal (raw-channel block + native deletion + bridge pin), resolveRelativePath (OPFS ".." collapse), composeApp + stripMetaRefresh (App iframe breakout/navigation defense), isServiceWorkerSender (actor-command source pin), normalizeRequest + needsWebWriteConfirm (WebVM bridge scheme/CRLF/auth/confirm)
- Verified in the browser by: `extension/tests/unit/engine-tabs/notebook-tab/notebook-seal.test.js (real worker realm); extension/tests/unit/offscreen/job-runner.test.js (a2a run denied egress + delegation); extension/tests/unit/red-team/sandbox-escape.test.js (in-browser red-team framing); scripts/cdp/states.mjs actor-command-sender-pin (live engine-tab forgery)`
- Defenses exercised: applyRealmSeal (raw-channel block + native deletion + bridge pin), resolveRelativePath (OPFS ".." collapse), buildWorkerSource + formatEvalResult (remote graph capability collapse + output fence), composeApp + stripMetaRefresh (App iframe breakout/navigation defense), isServiceWorkerSender (actor-command source pin), normalizeRequest + needsWebWriteConfirm (WebVM bridge scheme/CRLF/auth/confirm)
- Verified in the browser by: `extension/tests/unit/engine-tabs/notebook-tab/notebook-seal.test.js (real worker realm); extension/tests/unit/offscreen/job-runner.test.js (a2a run denied egress + delegation); tests/peerd-engine/module-resolver-toolbox.test.ts (remote-to-local toolbox refusal); tests/engine-tabs/notebook-tab/worker-caps-profile.test.ts (remote whole-run profile); tests/peerd-runtime/tools/remote-import-policy.test.ts (remote output fence); extension/tests/unit/red-team/sandbox-escape.test.js (in-browser red-team framing); scripts/cdp/states.mjs actor-command-sender-pin (live engine-tab forgery); scripts/cdp/states.mjs notebook-remote-restricted (live visible-Notebook host wall)`

| Probe (adversary action) | Result | Evidence |
|--------------------------|--------|----------|
Expand All @@ -165,6 +165,8 @@ _Generated from the current checkout by the command above._
| unseat the fetch bridge (assign/delete/defineProperty) | blocked | defineProperty on the non-configurable slot threw; bridge unchanged |
| reassign XMLHttpRequest to a working native | blocked | NotebookEgressBlockedError: XMLHttpRequest is disabled in the peerd Notebook |
| traverse OPFS out of the instance root via ../ imports | blocked | all '..' collapsed (e.g. "../../../../../../etc/passwd": "etc/passwd") |
| inherit ambient capabilities through a remote module | blocked | the entire graph uses the compute-only profile |
| break the model trust fence through remote return, console, or error text | blocked | host status stays outside a neutralized untrusted envelope |
| embed </script> in an inlined App worker to break out of the shim | blocked | worker source `<` escaped to \u003c, no executable breakout tag |
| meta-refresh the App frame to an attacker URL | blocked | meta http-equiv=refresh stripped from the app HTML |
| replay a broadcast actor command from a first-party engine tab | blocked | exact service-worker source accepted; same-extension tab provenance rejected |
Expand Down
31 changes: 22 additions & 9 deletions docs/security/THREAT-MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,19 +210,25 @@ Defenses (partial): there is no npm runtime inside the extension. Third-party co
vendored in `vendor/` with a `SOURCE.txt`. The Moonshine voice model is SHA-384
SRI-verified and refuses to load on a null SRI
(`peerd-runtime/voice/model-store.js`). Store and web builds refuse direct
remote JavaScript imports without requesting the module source. Preview literal
static remote modules cross the audited web-fetch path, have source and graph
caps, and may carry an optional SHA-256 pin. Dynamic imports are refused in
every package. Remote modules run inside a sealed worker. The store build strips
remote JavaScript imports without requesting the module source. On package
targets where Preview enables literal static remote modules, they cross the
audited web-fetch path, have source and graph
caps, and may carry an optional SHA-256 pin. Any remote module marks the whole
resolved graph as untrusted compute. Runtime network and file access, agents,
model calls, browser and site clients, and dweb are disabled for that run. A
remote module cannot import a local toolbox module. Generated worker shims and
independent host relay checks enforce the same profile. Returned values,
console output, and errors are fenced as untrusted.
Pins verify exact bytes and improve reproducibility. They do not grant trust or
authority. Dynamic imports are refused in every package. Remote modules run
inside a sealed worker. The store build strips
the `debugger` permission and the dweb module, and CI verifies zero dweb traces.
Accepted residuals: the CheerpX WebVM streams its root filesystem image from a
third-party host over WSS, which cannot be SRI-pinned. In Preview, an unpinned
HTTPS module can change at its publisher's discretion and inherits the execution
lane's capabilities. Visible Notebook results also lack remote-import provenance,
so remote-controlled output is not fenced before the Notebook actor reads it.
HTTPS module can change at its publisher's discretion.
The separate Store risk where code is fetched as data and then executed through
a local JavaScript or WebAssembly surface is tracked on the issue board.
Proven by (partial): scenario 06 for sandbox confinement of whatever the VM runs.
Proven by: scenario 06.

---

Expand Down Expand Up @@ -315,8 +321,15 @@ at an opaque origin (the manifest sandbox omits `allow-same-origin` and
`allow-top-navigation`) with all `chrome.*` stripped, and its inlined worker source is
escaped against a `</script>` breakout. The WebVM's only network path is an HTTP bridge
that refuses non-http(s) schemes, scrubs CRLF header injection, drops any smuggled auth
field, and confirms body-bearing verbs.
field, and confirms body-bearing verbs. If a resolved Notebook or Script graph
includes remote code, the entire run uses the compute-only profile. The resolver
blocks remote access to local toolbox modules. The worker and host both refuse
every authority-bearing relay, and the tool boundary fences all remote-controlled
output.
Code: `engine-tabs/notebook-tab/notebook-neutralizers.js` (`applyRealmSeal`),
`engine-tabs/notebook-tab/worker-source.js`,
`engine-tabs/notebook-tab/notebook-tab.js`, `offscreen/job-runner.js`,
`peerd-runtime/tools/defs/js-notebook.js`, `peerd-runtime/tools/defs/script.js`,
`peerd-engine/app-compose.js`, `peerd-engine/vm-net/http-bridge.js`,
`peerd-engine/module-resolver.js`, and the manifest sandbox CSP. Red-team: scenario 06,
with the real-realm proof in
Expand Down
2 changes: 1 addition & 1 deletion docs/store/PERMISSION-JUSTIFICATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ The Store Script and Notebook resolver refuses direct HTTP and HTTPS JavaScript
imports without requesting the module source. The package generates
`REMOTE_MODULE_IMPORTS_ENABLED = false`, both hosts consume it, and the
resolver requires an explicit true value before it can use the audited fetch
relay. Preview keeps that import path.
relay. Supported Preview targets keep that import path.

This does not settle every remote code question. A run can still fetch bytes as
data and later use local JavaScript or WebAssembly execution surfaces. The open
Expand Down
4 changes: 2 additions & 2 deletions docs/store/REVIEWER-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ The Store package refuses direct HTTP and HTTPS JavaScript imports in Script
and Notebook without requesting the module source. A generated package constant
disables the path in both execution hosts, the resolver fails closed when the
constant is absent or false, and artifact verification checks both Store
packages. Preview is a separate distribution and keeps audited literal static
remote imports. Dynamic imports are refused in every package.
packages. Chrome Preview is a separate distribution and keeps audited literal
static remote imports. Dynamic imports are refused in every package.

This does not establish that Store cannot execute network-derived code. The
open fetched-data execution policy covers data that a run later gives to local
Expand Down
2 changes: 1 addition & 1 deletion extension/background/offscreen-js-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const makeOffscreenJsClient = ({ ensureOffscreen, sendMessage }) => ({
* workspaceSessionId mounts the durable per-session workspace as the job's
* OPFS root (trusted job param — the tool derives it from ctx.session, the
* worker can never name its own root).
* @returns {Promise<{ value: unknown, consoleOutput: {level:string,text:string}[], durationMs: number, error: string|null, errorCode?: string, usedEgress?: boolean, usedActors?: boolean, actorDeliveryIds?: string[], usedWorkspace?: boolean, workspaceOverBudget?: boolean, actorsTrace?: Array<{ seq: number, method: string, to?: string, goal?: string, ok: boolean, ms: number, error?: string, settled?: boolean, actorFailed?: boolean, cancelled?: boolean }>, usedProvider?: boolean, providerCalls?: number, providerTokens?: number }>}
* @returns {Promise<{ value: unknown, consoleOutput: {level:string,text:string}[], durationMs: number, error: string|null, errorCode?: string, usedEgress?: boolean, usedRemoteModules?: boolean, usedActors?: boolean, actorDeliveryIds?: string[], usedWorkspace?: boolean, workspaceOverBudget?: boolean, actorsTrace?: Array<{ seq: number, method: string, to?: string, goal?: string, ok: boolean, ms: number, error?: string, settled?: boolean, actorFailed?: boolean, cancelled?: boolean }>, usedProvider?: boolean, providerCalls?: number, providerTokens?: number }>}
*/
execHeadless: async (code, { timeoutMs, a2a, ownerSessionId, actors, ownerToolUseId, runId, caps, siteFetch, toolbox, workspaceSessionId, signal } = {}) => {
const wallMs = typeof timeoutMs === 'number' && Number.isFinite(timeoutMs)
Expand Down
Loading