本指南描述当前公开 API 的真实行为。它区分安全默认产品、显式 agent 产品和实验性 iSH 产品,并给出从本地 RootFS 到一次性命令结果的完整闭环。
Caution
固定的 v0.4.0-abi.6 会 soft-halt 并 join embedded kernel,然后从
prepared.system.shutdown() 返回 Swift。成功后同一宿主进程不能再次 boot;不要把它
放在页面退出、scene 切换、deinit 或无意触发的普通清理路径中。
PocketRoot 暴露八个产品:
| 产品 | 用途 | 是否包含真实 iSH |
|---|---|---|
PocketRootCore |
状态、配置、命令、结果和错误模型 | 否 |
PocketRootResources |
RootFS 清单、校验、解包和安装 | 否 |
PocketRootTerminal |
UIKit 终端占位 UI | 否 |
PocketRootAgent |
provider-agnostic 有界 agent loop 与 OpenAI Responses transport | 否 |
PocketRootAgentRuntimeTools |
审批与策略保护的 Linux command adapter | 否 |
PocketRoot |
默认伞形产品,重新导出 Core、Resources 与 Terminal | 否 |
PocketRootIshRuntime |
固定 IshEmbed 的实验性原生适配 | 是 |
PocketRootIshRuntimeIntegration |
RootFS 安装器与原生适配的组合入口 | 是 |
仅构建业务模型或 UI 时依赖 PocketRoot。需要 agent loop 时额外显式依赖
PocketRootAgent;需要把已准备的 system 作为审批保护的命令工具交给 agent 时,再显式依赖
PocketRootAgentRuntimeTools。具体门禁见轻量 Agent Loop。要启动真实 guest,至少显式依赖
PocketRootIshRuntimeIntegration;示例还直接读取
PocketRootIshRuntimeFactory.isAvailable,因此同时列出 PocketRootIshRuntime 产品。
项目尚未发布稳定 Git tag。远程接入应固定到经过审核的完整 commit:
dependencies: [
.package(
url: "https://github.com/jacklv-coder/PocketRoot.git",
revision: "<reviewed-full-commit>"
)
],
targets: [
.target(
name: "YourAppFeature",
dependencies: [
.product(name: "PocketRoot", package: "PocketRoot"),
.product(name: "PocketRootAgent", package: "PocketRoot"),
.product(
name: "PocketRootAgentRuntimeTools",
package: "PocketRoot"
),
.product(name: "PocketRootIshRuntime", package: "PocketRoot"),
.product(
name: "PocketRootIshRuntimeIntegration",
package: "PocketRoot"
)
]
)
]在 Xcode 图形界面中:
- 选择 File → Add Package Dependencies;
- 输入
https://github.com/jacklv-coder/PocketRoot.git; - 在首个 release tag 前使用 Commit 规则并填入已审核完整 SHA;
- 默认接入选择
PocketRoot; - 真实 runtime 另外选择
PocketRootIshRuntime和PocketRootIshRuntimeIntegration。
本地开发可使用:
.package(path: "../PocketRoot")不要在首个 release tag 之前写 from: "0.1.0",也不要依赖浮动分支作为可复现的生产输入。
真实 driver 只有在以下编译条件同时满足时可用:
os(iOS);arch(arm64);- 能导入固定的
IshEmbed产品。
import PocketRootIshRuntime
guard PocketRootIshRuntimeFactory.isAvailable else {
// 在已经能够链接本产品的构建中检查 native driver 是否可用。
return
}macOS fallback 的作用是让宿主测试可以编译 API seam,不代表 macOS 支持 Linux guest。
更重要的是,isAvailable 运行在 SwiftPM 选择并链接依赖之后,不能作为 x86_64
Simulator 的降级开关。链接实验产品的 App target 必须像仓库内 spike/smoke target
一样设置 EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64,或者拆成不依赖实验产品的
独立 target。Intel Mac 不能构建当前原生 target。
调用方负责取得一个经过许可证和来源审查的本地普通文件。PocketRoot:
- 不读取远程 URL;
- 不请求网络权限;
- 不自动下载;
- 不把 RootFS 加入 bundle;
- 不接受符号链接或特殊文件作为归档输入。
取得 Application Support 目录:
import Foundation
let applicationSupportURL = try FileManager.default.url(
for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)localReviewedArchiveURL 不是 PocketRoot 自动生成的变量。产品可以通过
Files/document picker、受控下载或开发期 Simulator 注入取得文件。对于
document picker 返回的 security-scoped URL,建议先复制到 App 自己的
Application Support,再交给 installer:
func importRootFSArchive(
from importedURL: URL,
applicationSupportURL: URL
) throws -> URL {
let accessed = importedURL.startAccessingSecurityScopedResource()
defer {
if accessed {
importedURL.stopAccessingSecurityScopedResource()
}
}
let fileManager = FileManager.default
let inboxURL = applicationSupportURL.appendingPathComponent(
"RootFSInput",
isDirectory: true
)
try fileManager.createDirectory(
at: inboxURL,
withIntermediateDirectories: true
)
let localURL = inboxURL
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("tar.gz")
try fileManager.copyItem(at: importedURL, to: localURL)
return localURL
}
let localReviewedArchiveURL = try importRootFSArchive(
from: documentPickerURL,
applicationSupportURL: applicationSupportURL
)受控下载也应先完成到 App 自有的唯一临时/持久路径,再把该本地 URL 传入。
调用方负责网络、认证、许可证、文件保护、备份排除和清理策略。installer 会
再次要求输入为真实普通文件,并在自己的私有 staging 中建立快照。prepareSystem
返回后,调用方可按产品策略删除 RootFSInput 中的导入副本。
准备系统:
import PocketRoot
import PocketRootIshRuntimeIntegration
let prepared = try await PocketRootIshSystemFactory.prepareSystem(
archiveURL: localReviewedArchiveURL,
applicationSupportURL: applicationSupportURL,
workDirectory: "/",
maximumStandardOutputBytes: 8 * 1_024 * 1_024,
maximumStandardErrorBytes: 4 * 1_024 * 1_024
)返回值包含:
prepared.installation.version:固定清单版本;prepared.installation.rootFSURL:物化后的 fakefs 目录;prepared.installation.reusedExistingInstallation:是否复用已有有效安装;prepared.system:仍处于.idle、尚未启动的实验性系统。
默认安装布局:
<Application Support>/
└── rootfs/
├── current.json
└── v0.3.3/
├── .pocketroot-rootfs.json
├── meta.db
└── data/
归档中的顶层 fs/ 只用于输入布局校验;installer 提升的是这个目录本身,因此最终
v0.3.3/ 下直接是上述三个条目。复用以版本目录和其中的安装记录为准;即使
current.json 缺失或不匹配,有效版本仍会复用,并在返回前修复该索引。
实际安全算法见 RootFS 安全方案。
let system = prepared.system
try await system.boot()
guard await system.state == .ready else {
throw PocketRootError.runtimeFailure("Runtime did not become ready.")
}boot() 在原生 boot 返回后自动执行固定的 post-boot identity command。healthCheck 省略或为 nil 时,只有精确的内置 .ishEmbedV0_3_3 manifest 会自动选择同名健康配置,并严格要求 aarch64、alpine 和 3.19.1;自定义 manifest 默认使用不固定版本的 .alpineARM64,应显式传入与已审查 RootFS 对应的版本配置。架构、OS ID 和可选版本必须是非空且不含 NUL 的字符串,timeout 必须在 (0, 60] 秒,guest workDirectory 必须是无 NUL 的绝对路径;可选 supervisorGuestPath 也不能含 NUL,且会在占用进程槽位和进入原生 boot 前校验。
健康命令有独立的 4 KiB stdout/stderr 上限;os-release 作为数据由 Swift 解析,工作目录通过 argv 传入并以双方 pwd -P 结果比较,因此路径别名不会误判且预期值不会被插入 shell。失败发生在 native boot 之后时,runtime 保守进入 .failed 并消耗进程级槽位,必须重启宿主 App。该门禁是已验证 RootFS 内基础信息与命令上下文的一致性检查,不是独立的来源/安全证明,也不证明业务工具、网络或数据健康。健康 timeout 从 driver 入口覆盖 finite SPAWN、stdin close 与 event read;终止确认另有固定有界清理窗口。
IshEmbed 是进程级单例。即使创建多个 PocketRootSystem,同一个 App 进程中也只有一个对象能获得原生 runtime ownership。
let result = try await system.execute(
PocketRootCommandRequest(
command: "printf '%s' \"$POCKETROOT_MODE\"; uname -m",
workingDirectory: "/",
environment: ["POCKETROOT_MODE": "integration"],
timeout: .seconds(10),
mergeStandardError: false
)
)
if result.timedOut {
// read-loop deadline 已触发且已观察到 guest EXITED;结果可能包含此前收到的部分输出。
} else if result.signal != 0 {
// 子进程被 signal 终止。
} else if result.exitCode != 0 {
// shell 正常退出,但返回非零状态。
}
print(result.stdout)
print(result.stderr)- PocketRoot 实际执行
/bin/sh -lc <command>。 command是 shell 字符串;来自用户或网络的输入必须由调用方正确转义,不能把它当成安全 argv API。- 当前同一个 runtime 一次只允许一个一次性命令。
workingDirectory和environment按请求传给 guest。- timeout 必须大于 0 且不超过 24 小时。
- 大于 0 但不足 1 毫秒的 timeout 会提升为 1 毫秒,避免上游把 0 毫秒解释为无限等待。
- driver 入口建立统一 timeout deadline;finite SPAWN 使用剩余时间,stdin close 和
event-read loop 复用同一 deadline。到期时尝试终止 session,成功返回时包含
timedOut == true和已经收集的输出。 - native control queue、session backlog 与 lifecycle reserve 已有界;deadline 到期后的
terminate 与权威
EXITED确认使用独立的固定有界清理窗口。 - command、cwd 和 environment key/value 不能包含 NUL;environment key 还必须非空且不含
=。这些输入在进入 native driver 前校验,避免 C 字符串静默截断。 - spawn 直接返回 not-running、protocol 或 broken-pipe 时,PocketRoot 无法证明
transport 与 guest 状态,runtime 会立即失败关闭。session 建立后的关闭 stdin、非
timeout 读取、请求 timeout 或产品输出超限都会终止并确认退出。v4 transport 将
supervisor rejection、broken pipe 与 native backlog overflow 作为类型化错误;
guest
exit 17是合法结果,负数EXITED是协议完整性失败。native backlog overflow 会请求有界 session 清理并保留 byte/frame 来源,但 voidsession.close()无法向 Swift 证明是否升级成 instance fail-close,所以 PocketRoot 会终结 process gate 并要求重启。 mergeStandardError == true时 stderr 合并到 stdout,standardError为空。- 默认 stdout 上限 8 MiB,stderr 上限 4 MiB;通过
prepareSystem参数调整。 - 超过输出上限会终止 session,并抛出
PocketRootError.commandOutputLimitExceeded。 - 取消执行命令的 Swift Task 会请求终止 native session;只有确认可信
EXITED后才抛CancellationError,成功取消后 runtime 保持.ready。无法确认清理时会抛 runtime failure 并失败关闭。取消不回滚命令此前已经产生的副作用。
PocketRootConfiguration.defaultWorkingDirectory 和 commandTimeout 当前不会自动覆盖每个 PocketRootCommandRequest 的字段。需要统一策略时,请在应用层构造请求工厂。
| 字段 | 语义 |
|---|---|
exitCode |
guest 进程退出码;超时时为实现定义的失败值 |
signal |
终止 signal;0 表示没有映射到 signal |
standardOutput |
原始 stdout Data |
standardError |
原始 stderr Data |
stdout / stderr |
使用 UTF-8 replacement decoding 的便捷字符串 |
timedOut |
是否由请求 timeout 触发终止 |
下表描述 runtime 状态机。当前公开的 PocketRootSystem.state 在
boot()、shutdown()、execute() 返回或抛错后只发布稳定状态;调用仍在执行时,
外部轮询可能继续看到操作前的值,不能把它当作实时进度流。
命令若因无法确认 guest 退出而失败关闭,execute() 抛错前的内部 .failed 会在抛错时
同步到公开 state。
底层 IshLinuxRuntime 会在 suspension 前更新自己的 .booting / .shuttingDown
过渡状态来阻止重入;若 actor 重入使另一调用在这时失败,公开刷新会忽略这些过渡值。
刷新还使用递增代次:较新的刷新开始后,较早取得但延迟返回的快照会被丢弃,不能把
较新的 .failed 覆盖回旧状态。
| 状态 | 含义 |
|---|---|
.idle |
已创建但尚未 boot |
.preparingRootFS |
公共状态预留;当前组合工厂在返回 system 前完成安装 |
.booting |
原生启动进行中 |
.ready |
可接受一次性命令 |
.shuttingDown |
关闭已开始,不接受新操作 |
.terminated |
v0.4.0-abi.6 soft shutdown 成功返回;同进程不能再次 boot |
.failed(String) |
启动或关闭失败,通常需要重启宿主 App |
do {
let result = try await system.execute(request)
consume(result)
} catch let error as PocketRootError {
switch error {
case .runtimeNotBooted:
// 先完成 prepare 与 boot。
break
case .rootFSUnavailable(let reason):
// 本地归档或 fakefs 布局不可用。
log(reason)
case .invalidCommandRequest(let reason):
// timeout 或 runtime 输出上限不合法。
log(reason)
case .commandOutputLimitExceeded(let stream, let limit):
log("\(stream) exceeded \(limit) bytes")
case .restartRequired:
// 当前进程不能再次启动原生 runtime。
break
case .runtimeFailure(let reason),
.unsupportedOperation(let reason):
log(reason)
}
}RootFS 安装阶段还可能抛出:
PocketRootRootFSValidationError;PocketRootArchiveExtractionError;PocketRootRootFSInstallationError。
不要只按本地化字符串分支;优先匹配类型和 enum case。
当前真实 iOS 路径:
// 等待所有命令结束后显式关闭这一进程唯一的 iSH lifecycle。
try await system.shutdown()
assert(await system.state == .terminated)重要约束:
- 关闭前必须等待在途一次性命令结束。
- 原生
shutdown()关闭 guest PID 1、soft-halt kernel、bounded join 后返回 Swift。 - 当前不能做 “shutdown 后 boot”。
- 不要在 background、scene disconnect 或 ViewController 生命周期里无意自动调用。
- shutdown 返回后可以完成宿主资源清理,但若还需要 Linux runtime,必须新建宿主进程。
PocketRootSession、PocketRootSessionConfiguration 和 PocketRootSessionEvent 目前只是 API 基础。真实 iSH driver 的 session 创建仍返回 unsupported;公共 PocketRootSystem 也尚未暴露交互 session 创建入口。
当前不可宣称支持:
- PTY shell;
- 持续输入和输出;
- resize;
- signal/EOF;
- SwiftTerm 连接;
- session cancellation;
- 多 session registry;
- close-before-shutdown 保证。
PocketRootTerminal 当前可作为 UIKit 展示组件使用,但它不是 PTY:
import PocketRootTerminal
let terminalViewController = PocketRootTerminalViewController(
configuration: PocketRootTerminalConfiguration(
placeholderText: "Linux terminal is not connected.",
prompt: "$ ",
allowsInput: true,
showsAccessoryView: true
),
theme: .dark
)
terminalViewController.appendOutput("Preparing local environment…")
terminalViewController.apply(
theme: PocketRootTerminalTheme(palette: .dark, fontSize: 16)
)
navigationController?.pushViewController(
terminalViewController,
animated: true
)这些 UIKit 操作在 MainActor 上执行。appendOutput 只更新 transcript,
clearOutput() 清空显示。即使 allowsInput == true,当前 accessory view 也
只回显输入并提示 runtime 尚未安装,不会把命令发送给 guest。真正 terminal
必须等待 PocketRootSession、PTY 和 SwiftTerm gate。
- deployment target 为 iOS 18.0 或更高。
- 真实运行目标是 arm64 iOS。
- Swift Package 固定到已审核完整 commit。
- 显式依赖实验产品,没有误用默认
PocketRootSystem.shared。 - RootFS 是本地普通文件,大小和 SHA-256 与清单匹配。
- RootFS 获取、许可和存储策略由 App 明确负责。
-
prepareSystem、内置 boot identity gate 和业务专属健康检查按顺序执行。 - 每个命令有正数 timeout,并处理非零 exit、signal、timeout 和输出超限。
- 产品接受 shutdown 后同一宿主进程不能再次 boot 的单 lifecycle 契约。
- 没有把 Simulator 结果当作真机或发行结论。
- 发布前完成发行与合规中的全部阻塞项。