@ detail-dev/shahara (英语)
用于撒哈拉公共控制平面 API 和代理数据的 TypeScript 客户端 飞机 20号节点(针对22号节点开发),零运行时间依赖,双轨 建立无害环境管理。 出版和部署的版本必须从发行收据中确定. 这个包暴露了被支持的消费者SDK,而不是操作员CLI.
安装公开发布 npm install @detail-dev/sahara; 预释放使用
@detail-dev/sahara@next。用于本地开发,使用一个包装的归档或工作空间
路径。 维护者通过审查的 SDK 版本发布
SDK 发布,独立于服务/运行时间发布.
沙盒手柄是不透明的字符串(sb_ 之后是16个小写十六进制数字,即使忘记也永远不再使用. 其他64位身份(执行,化身,流位置,
试图先发制人) bigint;线带小数线
并使他们变成正人。 答复文件严格解码:
拒绝未知字段, 匹配服务器的 deny_unknown_fields
合同.
import { SaharaClient } from "@detail-dev/sahara";
const client = new SaharaClient({
apiBase: "http://control-plane.internal/", // private local example only
proxyBase: "http://proxy.internal/", // required for files, tunnels and SSH material
token: process.env.SAHARA_TOKEN!,
// proxyToken: process.env.SAHARA_PROXY_TOKEN, // when the proxy token differs
});
使用TLS或加密地道进行公交或其他不可信交通. 受限制的可信任集群/VPC链接可能使用平文. 私人运行时间/ 帮助交通 单独的认证和栅栏要求; 该客户端不批准 通过公共无记名标志实现内部所有权。 每个有效的公共标志都有完全的服务使用权.
失职和错误
承认是确认的持久承诺。 超时,倒出
连接,或 5xx 是 含糊不清 请求可能已经提出。 持久性
变异返回一个持久 Ack 改为:createSandbox, (中文(简体) ), exec, (中文(简体) ), closeSandbox, (中文(简体) ), cancelExecution, (中文(简体) ),
ackPreemption在相同的呼叫者选择身份下重试或调和
在限定的预算范围内(默认为 retry) (中文(简体) ). 他们从来没有
以新的身份重新发布. heartbeatSandbox 重复只更新缓存
同一逻辑的沙盒,处理预算范围内的瞬时故障;它没有
延长绝对过期或提供持久的ACK。 文件写入执行一次有界限的尝试; mayHaveApplied 记录不清
发布反应,不重复播放或检查.
409表面为SaharaConflictError与服务器的kind- 包括: 已发现的、身份已与不同意图有关的情况.401是致命的(SaharaAuthenticationError从来没有重复过.404实值SaharaNotFoundError。 。 。 .- 错误的电线文档是
SaharaContractError(种类)"contract", (中文(简体) ), 从未再审). - 呼叫器取消通过
CallOptions或操作的 查询/绑定signal;中止的尝试永远不会被重审.
创建、准备、执行和关闭
沙盒形状固定为8 vCPU (8,000 mCPU), 32 GiB RAM
(34359738368字节),并有128个GiB稀疏的磁盘(137438953472字节). 没有
资源选择器属于 CreateSandbox; 汇总主机过度承诺是一个
分开的录取政策,不是每个沙盒的上限.
const id = "sb_0000000000000001"; // choose an unused service identity
await client.createSandbox(id, {
image: 1n, // published image identity available to this service
expires_at_ms: BigInt(Date.now()) + 3_600_000n,
metadata: { cohort: "detail-five" },
});
let sandbox = await client.getSandbox(id);
while (sandbox.phase === "Pending" || sandbox.phase === "Booting") {
await new Promise((resolve) => setTimeout(resolve, 500));
sandbox = await client.getSandbox(id);
}
if (sandbox.phase !== "Ready") throw new Error(`sandbox phase: ${sandbox.phase}`);
const executionId = 100n;
await client.exec(id, executionId, {
command: "/work/run.sh",
env: { MODE: "eval" },
});
let poll = await client.getExecution(executionId);
while (poll.status === "Accepted") {
await new Promise((resolve) => setTimeout(resolve, 500));
poll = await client.getExecution(executionId);
}
if (typeof poll.status !== "object" || !("Completed" in poll.status)) {
throw new Error("execution did not complete");
}
const exitCode = poll.status.Completed;
const output = await client.readExecutionOutput(id, executionId, {
stream: "stdout",
offset: 0n,
limit: 1_048_576n,
});
// output.bytes is bounded; output.complete means the process domain is quiescent.
对于大输出, 使用一个代理流而不是请求连续切片 :
const output = await client.streamExecutionOutput(id, executionId, { stream: "stdout" });
for await (const bytes of output.chunks) await sink.write(bytes);
const receipt = output.receipt(); // exact length + SHA-256, only after verified trailers
输出仍保留在来宾磁盘上. 代理认证 本地请求,
解析一次运行,然后从一个保留描述符中接取被绑定的块.
主动执行可以提供经过验证的快照 complete === false· ;
完成该快照不能证明进程域为 quescent 。 每个
所请求的拉动有配置的请求超时;调用器处理时间介于
拉不是闲时间 输出方法都不自动重试或退后
到控制飞机的字节路线. readExecutionOutput 最多还收
1 MiB在所配置的每试请求截止日期下. 使用 cancel() 用于
未消耗的流; 放弃一个活式的转发器取消其响应.
匹配的来宾/运行时间二进制必须使用当前保留输出格式, 包括靴子、沙盒、创造、化身和执行。 旧产出 名称不会从新读者处获得创建身份。 部署和 持续产出的采用需要单独的限定.
整个恢复过程的沙盒手柄保持稳定。 服务可持久地约束接受
执行 ID 到他们的第一次运行; 重试从不重瞄准它们 。 文件调用
解决每个请求的一次运行. creation 财务报告和审定财务报表 incarnation 是诊断
字段;心跳和SSH只取逻辑沙盒手柄.
普查准备页
固定前缀普查,要求每页最多500行,并遵循
next_cursor 通过终端页面,包括当该页面为空时:
let cursor: string | undefined;
let through: bigint | undefined;
let position: bigint | undefined;
const readyIds = new Set<string>();
do {
const page = await client.listSandboxesPage({ phase: "Ready", limit: 500, cursor });
if (through !== undefined && page.through !== through) throw new Error("cutoff moved");
if (position !== undefined && page.position !== position) throw new Error("prefix moved");
through = page.through;
position = page.position;
for (const row of page.sandboxes) readyIds.add(row.id);
cursor = page.next_cursor ?? undefined;
} while (cursor !== undefined);
这是历史性的 承诺前缀观察, 不是证明所有客人
或提供者的能力和详细工作已经过去。 检查精确
身份证, creation, (中文(简体) ), incarnation,固定资源,运行元数据以及
计数,然后保留单独的运行时间和拆卸证据.
事件和预先确认
const controller = new AbortController();
const signal = controller.signal;
for await (const frame of client.events(sandbox.id, { signal })) {
if (frame.type === "overflow") {
// cursor fell outside the retained window — reconcile by reading
continue;
}
const event = frame.event;
if (event.kind === "preemption") {
await client.ackPreemption(sandbox.id, event.attempt);
}
// frame.id is the durable cursor; persist it to resume with
// client.events(id, { lastEventId: frame.id })
}
执行器将运输故障与 Last-Event-ID,宽容
保存相框,检测出摊位, 结束在干净的关闭上.
TCP 隧道
const stream = await client.connectTcp(sandbox.id, 8080);
stream.write(payload);
stream.on("data", (chunk) => {
/* ... */
});
或转发回路端口——每个被接受的本地套接字都会得到自己的地道 流线 :
const forward = await client.forwardLocalPort(sandbox.id, 8080);
console.log(`localhost:${forward.port}`);
await forward.stop();
逻辑沙盒的 SSH 材料
const ssh = await client.sshMaterial(sandbox.id);
// { user, host_public_key, port, private_key_pem }
// present only while the resolved runtime is live
消费者形象
for await (const entry of client.listImages()) {
const image = await client.getImage(entry.id);
console.log(image.id, image.phase);
// await client.deleteImage(image.id); // tombstones this shared catalog entry
}
// Public list/get include captured images and operator-imported base images.
// Every accepted public token sees the same catalog; there is no per-token owner.
// Public deletion uses the same catalog operation as operator deletion.
// Image import remains an operator API, not a public client operation.
来宾文件:有界流和最后收据
import { createHash } from "node:crypto";
import { createReadStream, createWriteStream } from "node:fs";
import { stat } from "node:fs/promises";
import { pipeline } from "node:stream/promises";
const local = "/tmp/report.json";
const { size } = await stat(local, { bigint: true });
const hash = createHash("sha256");
for await (const chunk of createReadStream(local, { highWaterMark: 1 << 20 })) {
hash.update(chunk);
}
const sha256 = hash.digest("hex");
const binding = {
path: "/home/user/report.json", // inside the default confined guest roots
length: size,
sha256,
mode: "0640",
};
const written = await client.writeFile(id, binding, () =>
createReadStream(local, { highWaterMark: 1 << 20 }),
);
if (written.size !== size || written.sha256 !== sha256) throw new Error("receipt mismatch");
const download = await client.readFile(id, {
path: binding.path,
});
await pipeline(download.chunks, createWriteStream("/tmp/roundtrip.json"));
const receipt = download.receipt(); // only available after final HTTP trailers verify
if (receipt.length !== size || receipt.sha256 !== sha256) {
throw new Error("file changed between upload and download");
}
await client.closeSandbox(id); // logical close; verify physical teardown separately
上传源头在每次明确通话时都会被消耗. 委托人把范围拉大了
拖取并只接受匹配大小/ digest 出版收据 。 丢失的答复
可能隐藏已公布的文件:检查 mayHaveApplied,并决定是否另作决定
独立写作是适当的。 没有公开转移身份或检查
终点. writeFileBytes编号 :writeFileText 处理呼叫者拥有的小型缓冲器;
readFileBytes 需求 maxBytes 并在返回前验证终端完整性.
流出大型文件和舞台下载字节直至 receipt() 成功 .
来源必须是 HTTP( S) 根源, 没有用户信息、 路径前缀、 查询或
碎片。 重定向已禁用 。 文件使用代理证书. 普通JSON语
30 秒 15次尝试预算 250毫秒指数
5秒后就关闭了 SSE 重接连接 EOF 和部分
框架来自上一个完全解码光标; 只交付应用程序事件
重新设置其连续失败窗口 。 空成功打开无法重置 .
A 类 closed 事件或取消结束订阅。 串流设置包括
- 健康订阅没有终身总上限.
上传有单独的 30分钟 完全尝试的截止日期,包括字节和发布回复。 覆盖 attemptTimeoutMs 在流媒体或字节/文本上传;这绝不允许重放。 普通的JSON电话保留了较短的重试预算.
文件下载使用 retry.requestTimeoutMs (默认时为15秒) 对于单取设置,包括任何拒绝体,然后45秒闲置读取,没有完全健康-流的最后期限. 闲置的截止时间仅在执行者请求字节或终端拖车时运行,因此下游回压不会超过健康的一流. 打电话 cancel() 或中止信号以阻止它; 部分文件永远不会被重放.
认证和装订检查
- 无效的公共令牌失败
SaharaAuthenticationError;所有有效符号共享服务访问. - 重用具有不同不可改变意图冲突的操作ID. 接受的工作从未默默地跟随康复.
- 烂透了
expected_sha256_before先决条件不能覆盖已更改的目的地. - 路径仍然局限于服务器允许的来宾根.
- 出处测试包括单试出版,粘糊糊糊的模糊性,坏坏的收据和拖车故障等.
沙盒事件流上的压力观测结果为
event.kind === "pressure" 边框( e)resource, (中文(简体) ), level, (中文(简体) ), cleared, (中文(简体) ), value, (中文(简体) ),
threshold, (中文(简体) ), since_ms) (中文(简体) ).
从详细单曲播放
建造焦油球并安装所保留的, 作为文件依赖的源头类文物 :
cd clients/typescript
pnpm install --frozen-lockfile && pnpm check:generated && pnpm build
pnpm pack && pnpm smoke:installed
单位 apps/backend/package.json数字 :
{
"dependencies": {
"@detail-dev/sahara": "file:../../vendor/detail-dev-sahara-<VERSION>.tgz"
}
}
或者销售整个软件包目录并使用 pnpm 工作空间 link: 编号 :
file: 找到它的道路。 货船 dist/esm 与 .d.ts 声明。 普通JS消费者必须使用动态 import()。 。 。 .
复兴合同
pnpm generate 运行 cargo xtask contracts 重播和重播时
src/generated/* (类型+代码+操作表). pnpm check:generated
漂移失败; 在服务器侧 API 更改后在 CI 中运行 .
测试
pnpm test # unit tests + contract suite against owned postgres + valkey
# containers plus sahara-sim-runtime + sahara-control-plane
# (binaries built at the repo root; requires Docker)
将套房定位为真正的部署——例如
cargo xtask physical-e2e --hold 集合,它印出每一个这些
设定 :
| 变量 | 含义 |
|---|---|
SAHARA_API | 公共 API 基础; 逗号分离的复制基地被接受, 领导者通过 /metrics |
SAHARA_TOKEN | 公共 API 无记名符号 |
SAHARA_PROXY | 代理数据飞机基础(可进行隧道/SSH合同测试) |
SAHARA_PROXY_TOKEN | 当代理无记名符号与 SAHARA_TOKEN |
SAHARA_OPERATOR_TOKEN | 运算符无记名符 (默认为 SAHARA_TOKEN(中文(简体) ) |
SAHARA_IMAGE_ID | 已发布图像 ID 沙盒规格应参考 |
SAHARA_ID_BASE | 第一个呼叫者选择的沙盒身份( 默认为100000); 重跑时与状态部署相撞 |
SSE恢复ID是精确的字符串:服务输出出一个20位零加法u64位置,一个连字符,以及一个u64正数加法到至少四个数字. 承诺量较大的批量可释放出更宽的正弦,如一万个. 这两个字段被限定为20个小数位和u64最高(1844644073709551615). 客户端也接受未加限的十进制对等,但呼叫者应当不加修改地通过收到的光标,而不是构造或使光标正常化. 客户端发送时保留其精确的字节 Last-Event-ID; 贴花与JSON DTOs中使用的克尼克十进制字符串不同.