本頁由機器翻譯,可能有錯誤。 閱讀英文原文

API

標準模組

Native Shell 為 TSX 提供十一個標準模組,以裸名稱匯入(Rust 程式碼使用 Rust 自己的函式庫:從 Rust 使用):這些是與 gpui-shell 相容的 Node 風格 API,在應用程式的 VM 中於 NSH 的原生執行階段上運行,其能力由 Rust 檢查。位於 .nsh/types 的 TypeScript 宣告與模組由同一份規格產生,因此型別所顯示的內容,就是執行階段實際匯出的內容。

模組匯出
bufferBuffer、atob、btoa、constants、default (LLRT);Buffer 同時也是全域變數
cryptocreateHash、createHmac、randomBytes、randomFill、randomFillSync、randomInt、randomUUID、getRandomValues、webcrypto、crypto、Crc32、Crc32c、Md5、Sha1、Sha256、Sha384、Sha512、default (LLRT);crypto 同時也是全域變數
pathbasename、delimiter、dirname、extname、format、isAbsolute、join、normalize、parse、relative、resolve、sep、default (LLRT)
urlURL、URLSearchParams、domainToASCII、domainToUnicode、fileURLToPath、format、pathToFileURL、urlToHttpOptions、default;URL 與 URLSearchParams 是 NSH 的全域類別
zlibdeflate、deflateRaw、inflate、inflateRaw、gzip、gunzip、brotliCompress、brotliDecompress、zstdCompress、zstdDecompress,各有其 Sync 形式,default (LLRT)
osplatform()、arch()、homedir()、EOL、default
consoledefault:VM 的 console(log、info、debug、warn、error)
fs/promisesreadFile、writeFile、readdir、exists、mkdir、rename、unlink、rmdir、stat、lstat、readlink、default
processrun、spawn、exit、platform、arch、nextTick、ChildProcess、default;default 物件另有 env 與 dataDir,並且就是全域的 process
netconnect、Socket、default
websocketconnect(url, options?) 解析為 WebSocketConnection(read、write、close(code?, reason?)、closeCode、closeReason)、WebSocketConnection、default;在原生主機上另有全域的 WebSocket 建構函式

#從 Rust 使用

Rust 程式碼以 Rust 標準函式庫與 crates.io 完成相同的工作,並在應用程式自己的原生程式碼中執行:阻塞呼叫以 blocking(…) 在執行階段的執行緒上執行,非同步呼叫則以 background(…)(nsh_core::prelude)執行,其結果會在一個 task 中回到元件。nsh.json 的 capabilities 規範應用程式的 TSX 與 NSH 的服務(fetch、剪貼簿);應用程式自己的 Rust 程式碼則以應用程式處理程序的權限執行。在瀏覽器應用程式中,Rust 程式碼以 fetch 存取網路,並以 nsh_core::storage::Storage 保存資料。

模組Rust
bufferVec<u8>、&[u8]、base64 crate
crypto如 sha2、hmac、rand 與 uuid 等 crate
pathstd::path
urlurl crate
zlib如 flate2、brotli 與 zstd 等 crate
osstd::env::consts、os()
consolelog crate
fs/promisesblocking(…) 中的 std::fs;鍵值資料使用 Storage
processblocking(…) 中的 std::process::Command
netblocking(…) 中的 std::net::TcpStream,或 background(…) 中的 tokio::net
websocket如 background(…) 中的 tokio-tungstenite 這類 crate

#匯入與產生的型別

每個模組都以表格中的名稱精確匯入:import { join } from "path"、import fs from "fs/promises"。打包工具會保留這些匯入,執行階段則以原生方式提供;目錄中就是這十一個名稱。buffer、crypto、path、url 與 zlib 是 LLRT 的實作,固定在 d9fd873e793c6551c5d92b2e31480f3279e3cd8d,其宣告由該版本投射到執行階段實際擁有的匯出上。其餘模組則是 NSH 自己的實作。NSH 保留其 URL、Blob、stream 與 event 類別的身分。

所有十一個模組都能在原生主機與瀏覽器應用程式中載入。Buffer、雜湊、加密與壓縮在 WebAssembly 中使用相同的可攜式 Rust 實作,包括同步 API;隨機值使用瀏覽器的密碼學熵來源。瀏覽器中的 path 操作使用 POSIX 路徑與虛擬的 / 工作目錄。瀏覽器檔案使用持久的應用程式私有儲存空間,WebSocket 則使用瀏覽器的傳輸層。在瀏覽器中,行程執行與原始 TCP 呼叫會以 code ERR_FEATURE_UNAVAILABLE_ON_PLATFORM 失敗。

#與 Node.js 及 gpui-shell 的比較

對於所匯出的子集,這些模組沿用 Node 的名稱與形狀;在 gpui-shell 有定義行為之處,則遵循 gpui-shell 的行為。NSH 自己的選擇如下:

  • fs/promises 的 exists 在符號連結的目標不存在時為 true,而 unlink、rename、rmdir、lstat 與 readlink 作用於符號連結本身。
  • Stats 與 Dirent 的欄位(size、mtimeMs、name 等)如同 Node 一樣是自有屬性,而 isFile()、isDirectory() 與 isSymbolicLink() 則位於其原型上。
  • ChildProcess 監聽器會收到事件的負載:輸出文字,或結束事件的 { code, signal }(當執行階段終止子行程時,還會帶有 error 與 errorCode)。on 會回傳子行程。run 解析為 { code, signal, stdout, stderr }:即 gpui-shell 的輸出,加上終止子行程的訊號。
  • 子行程的環境變數,是來自主機的一小組變數加上應用程式被授予的名稱;它在應用程式的資料目錄中執行,或在 fs.write 根目錄內的 cwd 中執行(處理程序)。
  • process.platform、process.arch、os.platform() 與 os.arch() 如同 gpui-shell 一樣,給出 Rust 的 std::env::consts 名稱:"macos"、"windows"、"linux"、"ios"、"android";"aarch64"、"x86_64"(作業系統中繼資料)。
  • 當主機沒有資料目錄時,process.dataDir 為 null;process.exit 會要求主機關閉應用程式介面。
  • 限制是每個 API 的一部分:大小、數量與逾時都列在下方各模組中。

#失敗

回傳 promise 的函式,會以拒絕(reject)該 promise 的方式回報所有失敗,如同 Node 的 fs/promises:無論是錯誤的引數、應用程式未被授權的路徑或主機、已用盡的限制,或是 I/O 錯誤,都一樣,因此一個 catch(或 .catch)就能處理全部。同步函式(process.spawn、process.exit、os.homedir、ChildProcess.write、全域 WebSocket 的建構函式與 send)則會拋出錯誤。錯誤的用法(型別錯誤的引數、超出範圍的選項)會是 TypeError 或 RangeError。應用程式能處理的每個失敗都帶有 code,它在各版本之間保持不變;訊息是寫給人看的,可能會變動,因此應用程式應依 code 分支處理:

code意義
ERR_ACCESS_DENIED應用程式的 capabilities 未授權此呼叫。錯誤帶有 permission(該能力在 nsh.json 中的鍵,如 "fs"、"process"、"network"),以及 resource(所要求的對象:路徑、指令(process.exit 時為 "exit")、socket 的 "host:port",或 WebSocket 的 URL)
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM此呼叫屬於其他平台:檔案可在桌面與瀏覽器中執行,行程可在桌面上執行,TCP socket 可在原生主機上執行,os.homedir() 則可在設定了家目錄的主機上執行
ERR_NSH_QUOTA_EXCEEDED已用盡執行階段的某項限制:進行中的操作或資料、子行程、socket、WebSocket、等待傳送的位元組、監聽器
ETIMEDOUT呼叫逾時:run 的 timeoutMs、連線、WebSocket 握手、寫入或關閉
ENOENT、EEXIST、EACCES、ECONNREFUSED 等作業系統的回應,使用 Node 的錯誤碼;EACCES 與 EPERM 是作業系統拒絕存取檔案,與應用程式的能力無關
ERR_OUT_OF_RANGE、ERR_FS_FILE_TOO_LARGE、ERR_SOCKET_CLOSED、ERR_CHILD_PROCESS_STDIO_MAXBUFFERNode 的錯誤碼,分別表示大小超出限制、連線已關閉,以及輸出超出 run 的擷取上限
import fs from "fs/promises";

async function loadSettings(): Promise<string> {
  try {
    return await fs.readFile("${dataDir}/settings.json", "utf8");
  } catch (error) {
    if (error.code === "ENOENT") return "{}";
    if (error.code === "ERR_ACCESS_DENIED") console.warn(`not granted: ${error.permission} ${error.resource}`);
    throw error;
  }
}

主機應用程式授予的權限可能少於應用程式所宣告的(沙箱與能力),因此應用程式處理 ERR_ACCESS_DENIED 的方式,應如同處理失敗的請求。

#作業系統中繼資料與日誌

os.platform(): string 與 os.arch(): string 回報實際的原生主機作業系統與架構,process.platform 與 process.arch 則持有相同的值。它們如同 gpui-shell 一樣,是 Rust 的 std::env::consts::OS 與 ARCH 的名稱:作業系統為 "macos"、"windows"、"linux"、"ios"、"android",架構為 "aarch64"、"x86_64",因此應用程式可直接與它們比較(process.platform === "macos")。os.EOL: string 是主機的換行分隔符。os.homedir(): string 回傳已設定的主機家目錄絕對路徑;若未設定,則拋出帶有 code ERR_FEATURE_UNAVAILABLE_ON_PLATFORM 的錯誤。目錄路徑屬於中繼資料:檔案操作仍然需要 fs.read / fs.write 授權。

瀏覽器應用程式讀到的是空的 process.env 物件,而 os.homedir() 在瀏覽器中會拋出 ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,因為家目錄路徑與環境變數屬於原生主機。

import console from "console" 共用 VM 的全域 console 物件。該模組只有預設匯出,支援 log、info、debug、warn、error,每個的簽章皆為 (...values: unknown[]): void。它使用 NSH 的記錄器格式。

#壓縮

zlib 來自 LLRT,另外加上 NSH 新增的一項界限:每次解壓縮(inflate、inflateRaw、gunzip、brotliDecompress、zstdDecompress 及其 Sync 形式)最多產生 64 MiB,或在 VM 的堆積限制更小時,以該限制為準。輸出更長時會拋出 RangeError,如同 Node.js 對 maxOutputLength 的行為;應用程式自己的 maxOutputLength 可為單次呼叫設定更低的上限。Buffer.alloc、Buffer.allocUnsafe、Buffer.allocUnsafeSlow 與 crypto.randomBytes 在大小超過 VM 的堆積限制(若無限制則為 1 GiB)時,會拋出 RangeError(code ERR_OUT_OF_RANGE);allocUnsafe 回傳已清零的記憶體。

#檔案

fs/promises 在桌面主機(macOS、Windows 與 Linux)上讀寫原生檔案,並在瀏覽器中透過 Origin Private File System (OPFS) 讀寫應用程式私有的瀏覽器檔案。每個路徑在操作開始前,都會依 capabilities.fs 檢查:readFile、readdir、stat、lstat、readlink 與 exists 使用 read 根目錄,writeFile、mkdir、rename、unlink 與 rmdir 使用 write 根目錄。每個失敗都會拒絕 promise:超出授權範圍的路徑會以 code ERR_ACCESS_DENIED(permission "fs",resource 為應用程式所寫的路徑原樣)拒絕,且發生在任何磁碟操作之前。

{
  "capabilities": {
    "fs": { "read": ["${dataDir}"], "write": ["${dataDir}"] }
  }
}

根目錄可以是絕對路徑、${dataDir}(此應用程式的資料目錄,由主機選定)、${pluginDir}(應用程式的目錄)、${homeDir},或相對於應用程式目錄的路徑。呼叫中的相對路徑會依序在每個被授權的根目錄中解析。內嵌的主機可以進一步縮小授權範圍。每個操作都停留在其被授權的根目錄內,符號連結也不例外;絕對路徑依其書寫方式檢查,因此根目錄要以它自己的路徑(或其解析後的路徑)存取,而不能透過位於其外的連結。lstat、readlink、exists、unlink、rename 與 rmdir 作用於符號連結本身:exists 在符號連結的目標不存在時為 true,而 unlink 會移除該連結並保留其目標。

失敗帶有 Node 的錯誤屬性:code(ENOENT、EEXIST、ENOTDIR、EISDIR、ENOTEMPTY、ELOOP、EACCES、EPERM 等;非 UTF-8 的文字為 EILSEQ,超過讀取限制為 ERR_FS_FILE_TOO_LARGE)、syscall 與 path,因此 error.code === "ENOENT" 可分辨出檔案不存在。超出授權範圍的路徑會以 ERR_ACCESS_DENIED 拒絕;在 iOS 或 Android 上的呼叫(其應用程式以 Storage 保存資料)會以 ERR_FEATURE_UNAVAILABLE_ON_PLATFORM 拒絕;錯誤的引數(未知的編碼或選項)則以 TypeError 拒絕。

readFile 回傳 Uint8Array,或在指定 "utf8" 時回傳字串,且可讀取最大 64 MiB 的檔案。writeFile 寫入最多 8 MiB 的字串或 Uint8Array,建立或取代一般檔案。readdir 列出最多 10 000 個名稱(其總量 1 MiB;更大的目錄會以 RangeError、code ERR_OUT_OF_RANGE 拒絕),並排序,或在指定 { withFileTypes: true } 時回傳 Dirent(name、isDirectory())。stat 與 lstat 解析為帶有 size、mode、mtimeMs / atimeMs / birthtimeMs、以 Date 表示的 mtime / atime / birthtime,以及 isFile()、isDirectory() 與 isSymbolicLink() 的 Stats。rmdir 移除一個空目錄。單一 VM 最多同時執行 1024 個操作,並最多持有 64 MiB 的進行中檔案資料(交給 writeFile 且尚未寫入的資料,以及已讀取但其 promise 尚未結算的資料);超出這些限制的呼叫會以 ERR_NSH_QUOTA_EXCEEDED 拒絕,而超過 8 MiB 的 writeFile 會以 RangeError(code ERR_OUT_OF_RANGE)拒絕。

瀏覽器儲存空間以頁面的 origin 與應用程式自己的命名空間為範圍,使用相同的檔案授權與大小限制,並在 Rust 中檢查。OPFS 需要安全環境,例如 HTTPS 或 localhost。檔案修改時間來自瀏覽器中繼資料;未知的存取/建立時間與模式為零。rename 使用瀏覽器控制代碼的原子移動,並在瀏覽器未提供該功能時回報 ENOTSUP。瀏覽器中的項目是一般檔案或目錄,而 readlink 回報 EINVAL。

#行程

process.spawn 與 process.run 在桌面主機(macOS、Windows 與 Linux)上啟動命令。執行階段會以引數陣列直接執行程式,不經過 shell,並擁有自己的管線;stdout 與 stderr 以增量 UTF-8 的形式送達。

{
  "capabilities": {
    "process": { "execute": ["git"], "env": ["GIT_AUTHOR_NAME"] },
    "fs": { "write": ["${dataDir}"] }
  }
}

命令必須與 process.execute 完全相符:git 與 /usr/bin/git 是各自獨立的授權。裸命令會透過主機 PATH 中的絕對目錄來解析;在 Windows 上,裸命令也會在那裡找到 <name>.com 與 <name>.exe。批次檔(.cmd、.bat)透過 cmd.exe 執行:請授權其絕對路徑(C:\Windows\System32\cmd.exe),並將 /d、/c 與該指令碼作為引數傳入。子行程的環境變數包含來自主機的 PATH、HOME、USERPROFILE、SystemRoot、TEMP、TMP、TMPDIR、LANG 與 LC_*,再加上 process.env 所授權的名稱(需完全相符,僅在 Windows 上忽略大小寫,因為其環境變數本就如此)。子行程會在應用程式的資料目錄中執行(若不存在則建立),或在 cwd 中執行,該目錄須為 fs.write 根目錄內既有的目錄,因為子行程可能寫入其執行所在的位置。嵌入式主機可以縮小授權範圍。

use std::{
    io::{BufRead, BufReader},
    process::{Command, Stdio},
};

spawn(async move {
    // `run`: the whole output once the command exits.
    if let Ok(Ok(output)) = blocking(|| Command::new("git").args(["status", "--short"]).output()).await {
        log::info!("{} {}", output.status, String::from_utf8_lossy(&output.stdout));
    }
    // `spawn` with `lines: true`: each line as it comes.
    let _ = blocking(|| -> std::io::Result<()> {
        let mut child = Command::new("git").args(["log", "--oneline"]).stdout(Stdio::piped()).spawn()?;
        if let Some(stdout) = child.stdout.take() {
            for line in BufReader::new(stdout).lines() {
                log::info!("{}", line?);
            }
        }
        log::info!("done {}", child.wait()?);
        Ok(())
    })
    .await;
});

run 無論結束狀態為何都會 resolve { code, signal, stdout, stderr }:code 為結束狀態,signal 為 null;若是由訊號終止子行程,則 code 為 null,signal 為訊號名稱("SIGTERM")。它在任何失敗時都會 reject。spawn 會立即回傳子行程;stdin: "pipe" 啟用 write 與 end,而 lines: true 則傳遞完整的行。on(event, handler) 回傳子行程,處理函式會收到事件的 payload:文字,或在最終輸出之後僅觸發一次的 { code, signal }(當執行階段終止子行程時,例如逾時或單行過長,則帶有 error)(之後才加入的監聽器會立即收到)。pid 是子行程的行程 ID。spawn 會在子行程啟動之前拋出 run reject 的內容:process.execute 未授權的命令會以 ERR_ACCESS_DENIED 失敗(permission "process",resource 為該命令),cwd 不在 fs.write 授權範圍內時會以 ERR_ACCESS_DENIED 失敗(permission "fs",resource 為 cwd),在桌面平台以外呼叫會以 ERR_FEATURE_UNAVAILABLE_ON_PLATFORM 失敗,超過子行程上限的呼叫會以 ERR_NSH_QUOTA_EXCEEDED 失敗;啟動失敗則帶有 code(找不到可執行檔時為 ENOENT,作業系統無法執行時為 EACCES)、syscall(spawn <command>)與 path。run 在截止時間以 ETIMEDOUT reject,輸出超過擷取上限時以 RangeError(code ERR_CHILD_PROCESS_STDIO_MAXBUFFER)reject;已啟動子行程的結束會在 error 中帶有相同的訊息,並在 errorCode 中帶有其代碼。process.dataDir 在主機沒有資料目錄時為 null。kill、逾時、reactive owner 的清理以及 VM 的拆除,都會結束整個行程樹:在 Unix 上,其行程群組會先收到 SIGTERM,250 毫秒後再收到 SIGKILL(拆除時則立即發送 SIGKILL)。

限制值
timeoutMs最高 4,294,967,295 毫秒;run 預設為 30 秒,spawn 則執行到結束
run 的擷取每個串流 8 MiB
使用 lines: true 時的單行8 MiB
每個 VM 的子行程數32
每個 VM 的處理中輸出8 MiB;讀取端會等待 JS 取走
每個子行程的待處理 stdin1 MiB;超過時 write 會拋出 ERR_NSH_QUOTA_EXCEEDED
監聽器每個子行程每個事件 64 個

process.exit(code?) 請求主機關閉此應用程式 surface。請在 capabilities.process 中以 "exit": true 授權;引擎會透過平台的應用程式結束命令轉送該請求。授權未涵蓋的呼叫會拋出 ERR_ACCESS_DENIED(permission "process",resource "exit")。主機透過其安裝的結束處理常式接收結束請求;在其他情況下,呼叫會拋出 ERR_FEATURE_UNAVAILABLE_ON_PLATFORM。在 macOS 上,這會關閉 surface 視窗。在瀏覽器中,它會停止應用程式並清除其 surface,同時保持主機頁面開啟。嵌入式主機控制其 surface 如何處理該請求。Gallery 拒絕此能力,因此其 Request host exit 動作用來示範權限處理,並讓示範保持開啟。

#網路權限與邊界

全域 fetch 保留 NSH 的串流 HTTP 實作,對請求與重新導向跳轉檢查 network.hosts 或 network.http。WebSocket 支援 ws: / wss:、文字與二進位訊息、事件監聽器以及關閉交握。WebSocket 交握是一個 HTTP GET,因此全域 WebSocket 與 websocket 模組的 connect 都使用 HTTP 授權:ws 以 http 檢查,wss 以 https 檢查,並使用 GET(規則也可以在 schemes 中列出 ws / wss)。NSH 在啟動連線之前會檢查初始的正規化 URL。原生交握會連線到該 URL 並拒絕重新導向;瀏覽器 WebSocket 的重新導向與連線政策由瀏覽器控制。兩個 API 都拒絕帶有憑證或片段的 URL。每個 VM 在兩個 API 之間最多持有 32 個 WebSocket,兩者的訊息大小上限皆為 8 MiB。

未授權給應用程式的 URL 會在任何連線開始之前被拒絕。全域 WebSocket 會以瀏覽器回報連線失敗的方式回報:建構函式會回傳該 socket,隨後它會觸發 error 與 close(代碼 1006,wasClean false),因此應用程式在建構函式之後立即設定的處理函式能收到它們。模組的 connect 會以 ERR_ACCESS_DENIED reject(permission "network",resource 為該 URL)。第 33 個 WebSocket 會以 ERR_NSH_QUOTA_EXCEEDED 拋出(建構函式)或 reject(connect),格式錯誤的 URL 則為 SyntaxError(建構函式)或 TypeError(connect)。

全域 WebSocket 最多佇列八則待傳送的訊息,緩衝最多 8 MiB(bufferedAmount);超過任一限制的 send() 會拋出 ERR_NSH_QUOTA_EXCEEDED,而一旦 socket 正在關閉,send() 會丟棄其資料,與瀏覽器的行為相同。其交握與每次寫入在 30 秒後逾時,關閉時最多等待 5 秒以取得對端的關閉框架。訊息抵達時會以 message 事件傳遞。

websocket 模組的 WebSocketConnection 最多持有八個待處理操作(讀取、寫入、關閉),以及最多 8 MiB 尚未傳送的寫入;超過任一限制的 write() 會以 ERR_NSH_QUOTA_EXCEEDED reject,超過 8 MiB 的訊息則以 RangeError(code ERR_OUT_OF_RANGE)reject。同一時間只能有一個 read() 在等待;第二個會以 TypeError reject。在被讀取之前抵達的訊息會等待應用程式,其流量取決於應用程式的讀取:當八則訊息或 8 MiB 未被讀取而在等待時,連線會停止從網路讀取,因此對端的傳送也會等待,並隨著應用程式讀取而恢復讀取。解析、連線與交握合計、每次寫入以及關閉,皆在 30 秒後逾時。

read() 依抵達順序 resolve 訊息。一旦連線已正常關閉(對端關閉,或應用程式呼叫 close()),且在其之前的每則訊息都已被讀取,read() 就會 resolve null,如同 net socket 在 EOF 時的行為;closeCode 與 closeReason 此時保存關閉框架的代碼與原因(連線開啟時為 null;沒有代碼的關閉框架為 1005;未經關閉交握就結束的連線為 1006)。read() 只在失敗時 reject,且在其之前的訊息之後,附帶其 code:未經關閉交握就結束的連線為 ERR_SOCKET_CLOSED,ETIMEDOUT 為 socket 錯誤的作業系統代碼,EPROTO 為協定錯誤(若交握被拒絕,則帶有 HTTP status)。close(code?, reason?) 在關閉框架中傳送其代碼(1000,預設值,或 3000 至 4999)與原因(最多 123 個 UTF-8 位元組),與全域 WebSocket 的 close() 相同;超出這些範圍的代碼或原因會以 RangeError(code ERR_OUT_OF_RANGE)reject。它會丟棄尚未讀取的訊息,並在對端回應關閉交握後 resolve。

import { connect } from "websocket";

const socket = await connect("wss://chat.example.com/socket");
await socket.write("hello");
for (let message; (message = await socket.read()) !== null; ) {
  console.log(message);
}
console.log(`closed: ${socket.closeCode} ${socket.closeReason}`);

net.connect({ host, port }) 僅使用 network.sockets,這是一份精確 "host:port" 目的地的清單(IPv6 為 "[::1]:9000"),會在 DNS 查詢之前檢查。每個項目授權一個主機上的一個連接埠;主機模式、network.hosts 與 HTTP 授權僅涵蓋 HTTP 流量,而沒有 "capabilities" 的應用程式也會列出其 socket。回傳的 Socket 提供 read(size?)、write(text) 與 close();讀取會回傳 Uint8Array,或在 EOF 時回傳 null。所有失敗都會 reject:未授權的目的地以 ERR_ACCESS_DENIED(permission "network",resource "host:port")reject,第 33 個 socket 以及超過可等待之 4 MiB 的寫入以 ERR_NSH_QUOTA_EXCEEDED reject,超過 1 MiB 的讀取或寫入以 RangeError(code ERR_OUT_OF_RANGE)reject;連線與 I/O 失敗以 Node 的 code(ECONNREFUSED、ENOTFOUND、ETIMEDOUT、ECONNRESET 等)與 syscall reject,連線失敗還會帶有 address 與 port,在已關閉的 socket 上寫入則以 ERR_SOCKET_CLOSED reject(在其上讀取則回傳 null)。連線與 I/O 有 30 秒逾時,每次讀取/寫入限制為 1 MiB,最多 4 MiB 的寫入可等待傳送,close() 會立即結束待處理的讀取與寫入。每個 VM 最多保持 32 個開啟或連線中的 socket。這是一個基於 promise 的用戶端 socket 介面。

原始 TCP 使用原生主機。兩種 WebSocket 介面在 Web 應用程式中都使用瀏覽器傳輸,並保留 Rust 能力檢查與上述限制。自訂的瀏覽器交握標頭會回報明確的不支援選項錯誤。瀏覽器連線政策也同樣適用。嵌入式授權會與應用程式的能力宣告取交集。

#WebSocket 授權範例

此 nsh.json 片段允許此 wss URL 的 GET 交握,並保留其路徑與 HTTPS 範圍:

{
  "capabilities": {
    "network": {
      "http": [{
        "host": "chat.example.com",
        "methods": ["GET"],
        "schemes": ["https"],
        "pathPrefix": "/socket"
      }]
    }
  }
}
use futures_util::{SinkExt, StreamExt};
use tokio_tungstenite::{connect_async, tungstenite::Message};

spawn(async move {
    let _ = background(async {
        let (mut socket, _) = connect_async("wss://chat.example.com/socket").await?;
        socket.send(Message::text("hello")).await?;
        while let Some(message) = socket.next().await {
            log::info!("{}", message?);
        }
        Ok::<_, tokio_tungstenite::tungstenite::Error>(())
    })
    .await;
});

#TCP 授權範例

TCP 使用 socket 授權:應用程式所連線的精確主機與連接埠。

{
  "capabilities": {
    "network": { "sockets": ["tcp.example.com:9000"] }
  }
}
use std::{
    io::{Read, Write},
    net::TcpStream,
};

spawn(async move {
    let reply = blocking(|| -> std::io::Result<String> {
        let mut socket = TcpStream::connect(("tcp.example.com", 9000))?;
        socket.write_all(b"hello\n")?;
        let mut bytes = [0; 1024];
        let n = socket.read(&mut bytes)?;
        Ok(String::from_utf8_lossy(&bytes[..n]).into_owned())
    })
    .await;
    if let Ok(Ok(reply)) = reply {
        log::info!("{reply}");
    }
});

在 examples/gallery 中開啟 Standard APIs,以執行檔案操作、子行程、TCP 與兩種 WebSocket 介面,並檢視主機中繼資料與已佇列的工作。utilities 動作會示範緩衝區、雜湊、壓縮與檔案 URL。每個動作都會顯示其結果或錯誤。檔案存放於 Gallery 自身資料目錄下的暫存目錄中,並在動作之後移除。TCP 與 WebSocket 動作會連線到連接埠 9876 與 9877 上的本機 echo 伺服器。在 Web 上,檔案動作會使用 OPFS,utilities 在 WebAssembly 中執行,兩個 WebSocket 動作都使用瀏覽器連線;行程與 TCP 動作會顯示主機有型別的平台錯誤。

Gallery 的 AI Chat 使用 process.spawn,在桌面上透過本機安裝的 Claude 或 Codex CLI 進行聊天。請在終端機中透過 CLI 登入,然後在輸入區選擇提供者。回覆會以對話作為上下文;Stop 與離開畫面都會結束子行程。Web 與行動裝置則執行腳本化的串流對話,包含推理、工具卡片與錯誤復原。