SDK 接入

拿到应用凭据,安装 SDK,向已连接的电脑发送第一个任务。

让应用后端、Agent 或自动化脚本调用指定电脑上已配置的 Harness。SDK 运行在 Node.js 22.19+,不直接运行在浏览器中;App v0.1.3 的设备端要求 macOS 15+、Apple 芯片;SDK 归档中的独立设备 CLI 为 macOS arm64,支持 Codex 与实验性 dsh。

完成第一个任务#

应用只需要 SDK 和管理员签发的 client.json,不安装设备驱动,也不用启动 Server 或 Daemon。设备用户安装 App 并通过邀请连接,见App 使用指南;还没有服务时,由管理员按Docker 部署准备一次即可。

SDK 0.1.2 Developer Alpha 已包含下面的简洁 API,与 macOS App 分别发行;不发布到 npm registry。

1. 安装 SDK,放好应用凭据

将该应用专属的 client.json 放入项目的 .local/。从固定发行地址安装 SDK;需要离线安装或先核验摘要时,见下载与校验步骤。将 .local/、node_modules/ 加入忽略规则;不要将凭据写入前端或提交到 Git。同一应用身份只允许一个活动连接,示例使用独立应用凭据。

sh
npm install --ignore-scripts --no-audit --no-fund https://github.com/hutian-ai/harness-dock/releases/download/sdk-v0.1.2/harness-dock-sdk-0.1.2.tgz

2. 连接并执行

保存为 hello.mjs,运行 node hello.mjs。只有一个获授权设备时自动选择;多设备时先用 dock.listNodes() 查看,再明确传入 nodeId。

ts
import { connectDock } from '@harness-dock/client';

const dock = await connectDock('.local/client.json');
try {
  const task = await dock.run({
    prompt: '回复 hello',
    receipt: '.local/hello.json', // 每个新任务使用一个新文件
    // nodeId: '从 dock.listNodes() 选取的设备 UUID',
    // provider: 'dsh', // 默认 codex;需设备已配置且可用
  });
  console.log(task.result); // 检查 state,原生结果字段因 Harness 而异
} finally {
  await dock.close();
}

run 自动保存私有执行记录、检查设备与 Harness、准备执行并发起一次任务。默认拒绝需要原生审批的请求;需要交互时传入 permission 回调,准确返回原生选项。收到 completed 仍需应用验收业务结果。SDK 不自动收集或删除产物。

3. 中断后查看原任务

保留原 receipt,使用相同应用凭据查询。已有 receipt 会阻止再次派发;超时或断线后不要换一个文件重新运行同一任务。

ts
import { connectDock } from '@harness-dock/client';

const dock = await connectDock('.local/client.json');
try {
  console.log(await dock.status('.local/hello.json'));
} finally {
  await dock.close();
}

status 只查询原执行,不发送 prompt。null 或 unknown 都表示缺少完成证据。close() 关闭应用连接,不代表取消设备上的任务。

常用 API 与进阶边界#

  • connectDock(path | options):读取应用配置,等待连接就绪;dock.close() 释放连接。
  • dock.listNodes():列出该应用获准访问的设备;多设备时显式选取,不自动切换或广播。
  • dock.run({ prompt, receipt, nodeId?, provider?, model?, timeoutMs?, permission? }):一次性文本执行,返回原生响应及 runId、nodeId、receipt。
  • dock.status(receipt):按原执行标识查询快照;不自动重派或原生查回。

每台设备同时只使用一个执行回调处理器;该简洁入口适合一次性文本任务,不与该设备的其他执行共享回调。文件上传、会话续接、调用方工具、事件订阅、取消、产物收集与释放使用 dock.client 的底层 API。完整执行边界见执行生命周期。

连接另一台 Mac 或已有服务#

已部署服务时,应用端只需下载并安装 SDK,取得管理员为该应用签发的 client.json,再运行同一个示例。设备用户从 安装指南下载 App、登录 Codex、通过邀请连接;服务端操作见 server 部署和 配对与访问管理。

跨机器需双方可达、证书受信任的 WSS/HTTPS 入口。不能把本节的 loopback 地址或邀请原样交给另一台电脑。每个应用身份只允许一个活动连接;用独立的应用凭据运行示例,避免与已运行的业务应用争用。

未发布源码:观察、收存与释放#

onReceipt 在私有记录落盘后、任何设备请求前提供原 runId;它不证明任务已经提交。progress 保留原执行事件,不能代替权威终态。signal 和超时仅停止本地等待及相关回调,不向设备发送 cancel;原生执行可能继续。

示例
import { connectDock } from '@harness-dock/client';
const dock = await connectDock('.local/client.json');
const observation = new AbortController();
try {
  const task = await dock.run({
    prompt: '回复 hello', receipt: '.local/hello.json',
    onReceipt: handle => console.log('保留原任务:', handle.runId, handle.receipt),
    progress: event => console.log(event.kind, event.runId),
    signal: observation.signal,
  });
  console.log(task.result); // 检查技术终态;业务结果仍由应用验收。
} finally { await dock.close(); }
// 需要停止观察时调用 observation.abort(),不是取消原生任务。
// 断线后按原 receipt 查询;不换 receipt 重新运行。

dock.collect(receipt) 先查询原执行,只在 completed、failed 或 cancelled 已确认时返回托管输出;应用校验 SHA256 并收存需要保留的结果。dock.release(receipt) 再次确认终态,并显式释放本轮输出和一次性托管会话;保留本地 receipt、原生历史和用户工作目录。null、running、unknown 均拒绝收集和释放。业务结果已经验收并保存后才调用 release;不要放进 finally。

候选 SDK 的 examples/quickstart.mjs 给出可分步运行的完整路径。run 保存原生结果及经摘要核验的输出归档,status 仅查询,collect 在恢复后补存归档;查看并接受归档后,另行运行 release。已有 receipt 或归档不会覆盖:

示例
node quickstart.mjs run
# 中断后只查原任务;已确认终态时可补存归档:
node quickstart.mjs status
node quickstart.mjs collect
# 打开 .local/hello-archive.json,验收并备份需要的内容后,再显式释放:
node quickstart.mjs release

日常自动化不能只创建新 receipt 而从不释放。七天详情回收以“已结束且已 release”为前提;长期未释放的记录和文件仍占设备预算,达到上限会拒绝新任务。每台设备只有一个执行回调处理器;简洁 run 不与该设备的其他执行共享回调。多任务并发需使用底层 API 按 runId 路由同一处理器。

候选 SDK:连接失败下一步

connectionError 与首次连接错误仅返回清理后的分类,不暴露 token、响应正文或证书文本。server_tls_rejected:核对 CA、有效期和主机名;server_dns_failed / server_connection_refused:核对解析、网络和监听;server_handshake_rejected:核对应用凭据、协议、反向代理及该身份是否已有活动连接。握手拒绝不透露具体身份检查条件,不能据此认定一定是凭据错误。server_connection_timeout 表示尚未取得连接证据;设备和业务执行是否就绪仍分别检查。

没有服务?在一台 Mac 上独立体验(已发布 SDK)#

本节让应用、server 和隔离的设备后台运行在同一台 Mac,验证公开下载到真实执行的完整路径;它不会修改已有 App 连接或开启登录自启。输入会上传到该设备,再由原生 Codex 读取并生成 Markdown,使用本机原生账号的模型额度。SDK 把最终正文保存到调用方目录。

1. 下载、校验并安装 SDK

在终端新建空项目目录。下面下载的六个文件来自同一个固定 SDK 发行,release.json 记录源码 revision 和构建来源;摘要检测下载损坏,不能替代发布者身份验证。

sh
mkdir harness-dock-first-task
cd harness-dock-first-task
umask 077
mkdir -p .local/downloads
printf '.local/\nnode_modules/\n' >> .gitignore
base="https://github.com/hutian-ai/harness-dock/releases/download/sdk-v0.1.2"
for file in harness-dock-sdk-0.1.2.tgz harness-dock-server-0.1.2.tar.gz harness-dock-daemon-0.1.2-darwin-arm64.tar.gz release.json README.txt SHA256SUMS; do
  curl --fail --location "$base/$file" --output ".local/downloads/$file" || exit 1
done
(cd .local/downloads && shasum -a 256 -c SHA256SUMS) || exit 1
npm init -y
npm install --offline --ignore-scripts --no-audit --no-fund ./.local/downloads/harness-dock-sdk-0.1.2.tgz
cp node_modules/@harness-dock/client/examples/first-task.mjs .
cp node_modules/@harness-dock/client/examples/notes.txt .
tar -xzf .local/downloads/harness-dock-server-0.1.2.tar.gz
tar -xzf .local/downloads/harness-dock-daemon-0.1.2-darwin-arm64.tar.gz

安装过程中不需要 npm 登录或访问 npm registry。SDK 随包提供协议依赖;调用方不会安装 Codex 驱动。notes.txt 是可替换的项目笔记,示例接受不超过 64 KiB 的 UTF-8 文本。

2. 终端一:创建私有服务并启动

以下命令都在新项目目录执行。.local/control 必须不存在;初始化会生成 server、应用和设备各自的私有配置,并把一个设备授权给这个应用。不要把 server.json 或 device-pairing.json 作为应用凭据。

sh
node server/dist/cli.js init .local/control --device-port 49518 --client-port 49519
node server/dist/cli.js run .local/control/server.json

保持此终端运行。若端口已占用,换两个空闲端口后重新初始化一个新私有目录;不要覆盖既有服务配置。此路径只监听 127.0.0.1,用于同机首次接入。

3. 终端二:启动隔离的执行设备

先在这台 Mac 安装并登录 Codex。这里用开发者 Daemon 归档建立独立设备身份和工作目录,不复用或停止既有 App 的后台。

sh
mkdir -p .local/device-work
node daemon/dist/cli.js init --home .local/device --cwd "$PWD/.local/device-work" --pairing .local/control/device-pairing.json
node daemon/dist/cli.js run .local/device/config.json

保持此终端运行。设备使用本机已有的 Codex 安装、登录与工具环境;run 不启用登录自启。

4. 终端三:运行应用示例,保存报告

sh
node node_modules/@harness-dock/client/dist/cli.js nodes .local/control/client.json
node first-task.mjs run .local/control/client.json --input notes.txt --output .local/report.md --receipt .local/first-task.json

仅有一个获授权设备时自动选中;多个设备时从 nodes 输出选取,并加 --node-id <UUID>。默认使用原生模型;可用 --model 显式选择设备报告支持的模型。

示例使用 config: { mode: "read-only" },由 Codex 读取上传的笔记并返回项目摘要、行动表和待确认问题,调用方保存 .local/report.md。如果原生请求审批,交互终端会展示原始选项,由你选择;非交互输入会拒绝审批。

text
{"runId":"...","state":"completed","output":".../report.md"}
Report saved: .../report.md

成功需要同时看到 state: completed 和保存的非空 Markdown 报告。打开报告核对它是否正确使用了笔记事实;连接成功或 inventory ready 都不能代替这个结果。这是文件整理任务,不是固定回显自检。

中断后查询原任务

示例在任何准备或执行请求之前,以 0600 权限保存原 runId、应用与设备身份、输入摘要和输出路径。原 receipt 已存在时拒绝再次派发;输出文件也不会被覆盖。

sh
node first-task.mjs status .local/control/client.json --receipt .local/first-task.json

status 只查询原执行;已确认完成时可补存报告。null、running、unknown、断线和超时都不能证明“未执行”,应保留 receipt 继续核对,不重新运行 run 来恢复。示例退出码:0 为报告已保存,1 为明确失败或输入错误,2 为执行结果未确认。Ctrl-C 只中断当前进程,不证明原生执行已取消。

成功收存报告后,示例释放本轮托管输出和上传的 session 文件;它不会删除原生 Codex 的会话历史。确认任务终态后,可用 node daemon/dist/cli.js stop .local/device/config.json 停止这个隔离设备,再在终端一按 Ctrl-C 关闭该 server。

底层 SDK API:安装与连接(已发布版本)#

已有服务的应用也可以直接安装固定发行的 SDK:

sh
npm install --ignore-scripts --no-audit --no-fund https://github.com/hutian-ai/harness-dock/releases/download/sdk-v0.1.2/harness-dock-sdk-0.1.2.tgz

应用领取自己的 client.json,不持有设备配对凭据。loadClientOptions 读取私有文件并处理相对 CA 路径。

ts
import { ServerClient, loadClientOptions } from '@harness-dock/client';

const client = new ServerClient(await loadClientOptions('.local/client.json'));
await client.ready;
console.table(client.listNodes());
client.onConnection(node => console.log(node.nodeId, node.connected));

listNodes 仅包含该应用获准访问的设备。每台设备连接一个控制方,每个应用身份只允许一个活动连接。

跑通首任务后,需要进一步核验会话续接、工具或原生审批时,可按需运行应用侧两轮执行测试。已有业务应用连接时,通过原应用验证;CLI 测试只在该身份没有其他活动连接时使用,不另建控制方。

一次执行#

示例
import { randomUUID } from 'node:crypto';
import { mkdir, open } from 'node:fs/promises';

const nodes = client.listNodes();
const peer = nodes.length === 1 ? client.node(nodes[0]!.nodeId) : undefined;
if (!peer?.connected) throw new Error('device_unavailable');
const runId = randomUUID(), sessionKey = randomUUID();
const input = { provider: 'codex', runId, sessionKey, prompt: '回复 hello' };
await mkdir('.local/raw-runs', { recursive: true, mode: 0o700 });
const file = await open(`.local/raw-runs/${runId}.json`, 'wx', 0o600);
try {
  await file.writeFile(JSON.stringify({ nodeId: peer.nodeId, runId, sessionKey, input }) + '\n');
  await file.sync();
} finally { await file.close(); }
console.log('已保存执行身份:', runId);

const daemonId = peer.daemonId;
const remove = peer.onExecutionCallback(async (request, context) => {
  context.signal.throwIfAborted();
  if (request.runId !== runId || context.nodeId !== peer.nodeId || context.daemonId !== daemonId)
    throw new Error('run_not_authorized');
  if (request.kind === 'permission') return 'deny';
  if (request.kind === 'toolCall') throw new Error('caller_tool_not_registered');
  return null;
});
try {
  const inventory = await peer.request('inventory');
  const codex = inventory.providers.find(provider => provider.key === 'codex');
  if (codex?.status !== 'ready') throw new Error('provider_unavailable');
  await peer.request('prepare', { runId, sessionKey, files: [] });
  const response = await peer.request('execute', input);
  console.log(response.result);
  // 保留身份。应用验收并收存结果后,再显式 collect/release/forgetSession。
} finally {
  remove();
  await client.close();
}

示例在任何设备请求前独占保存 runId、sessionKey 和原输入;后续查询继续使用同一身份。上述默认回调拒绝原生审批;实际应用需接入自己的授权与交互界面。只有确定执行结束后才释放托管文件。

方法#

应用通过 ServerClient 取得 ServerPeer 调用设备。普通 request 按方法推导参数与结果;显式 request<T> 可用于调用方自有投影,由调用方保证准确性。类型不替代运行时校验或 server 白名单。

方法输入与结果
inventory可选 refresh;返回安装、协议就绪、能力与活动执行
preparerunId、可选 sessionKey/files;返回托管目录
executeprovider、runId、prompt 及可选原生配置;返回执行结果
getExecution原 runId;返回本地快照或 null
eventsrunId、可选 after;返回有序事件
reconcile原 runId;按已保存上下文只读查回,未确认时为 null
cancel原 runId;只确认取消请求,不等于 cancelled 终态
steerrunId/text;返回 accepted、rejected 或 unknown
collect原 runId;返回带 SHA256 的输出文件
release原 runId;释放已确认结束的托管输出
forgetSessionsessionKey;释放托管会话,保留去重身份与用户文件

公共类型包含 DeviceInventory、DeviceExecuteInput、DeviceExecutionResponse、DeviceExecutionSnapshot、DeviceProviderCapabilities、DeviceExecutionCallback 及审批/工具类型。原生扩展字段保留 unknown;使用前按支持的后端校验。

原生交互与能力#

onExecutionCallback 为执行回调提供类型推导;onRequest 保留通用传输处理器签名。两个入口共享一个处理器槽位和连接有效期。

  • permission:options 精确返回 { optionId };form/url 返回 { action, content? }。保留原始问题和作用域,不猜测选项或默认 always。
  • toolCall:仅执行本应用为本轮注册并授权的工具,返回 { content, isError?, deliveryContext? }。
  • permissionDelivered/toolDelivery:确认协议投递;不代表工具成功或业务目标完成。
  • ready/event/progress 等:用于准入和观察,不能代替权威终态。

先检查 sessionResume、resultQuery、exactFinalAnswer、callerSkills 和 permissionInteractions。inventory 的 ready 只表示协议探测成功,modelExecutionVerified=false。

通过 CLI 执行任务#

需要先发送自己的任务时,可使用调用端 CLI。CLI 复用连接 SDK,由设备 Daemon 调用原生 Harness。安装本页 SDK 后,在调用方项目目录执行以下命令;源码开发也可使用 pnpm client。任务会使用原生模型账户额度。

使用专属的 .local/client.json 应用凭据。每个应用身份只允许一个活动连接;已有业务应用正在使用该身份时,不能再用 CLI 建立第二条连接。只在唯一获授权设备可明确选定时自动选择,否则增加 --node-id <设备 UUID>。

sh
node node_modules/@harness-dock/client/dist/cli.js run .local/client.json \
  --prompt "查看工作目录,说明主要入口;不要修改文件。" \
  --receipt .local/task-run.json \
  > .local/task-result.json

node node_modules/@harness-dock/client/dist/cli.js status .local/client.json --receipt .local/task-run.json
  • --prompt 与 --prompt-file .local/task.txt 二选一;文件内容作为原始任务输入。
  • --provider 默认 codex。仅在明确原生模型名时传 --model;省略时保留原生默认模型。设备与 Harness 的支持范围见支持范围。
  • --run-id 可显式指定 UUID,否则生成一次。提交前以私有权限独占创建请求记录,--receipt 指定新文件,默认位于 .local/client-runs/<runId>.json。记录含原始任务输入与身份,不含认证凭据,也不证明执行已完成;已有记录不会覆盖。
  • --timeout-ms 控制执行响应的等待,默认 3600000,最大 86400000。超时或 Ctrl-C 只停止观察,不代表原生任务已取消;保留请求记录,使用 status --receipt 查询原执行。

交互终端会逐项呈现原生审批,保留原始选项和作用域;非交互输入拒绝需要人工决定的请求,不自动允许。进度与审批写入 stderr,stdout 只输出一份最终 JSON,包含 runId、nodeId、provider、receipt、state 及原生结果或错误。

run 仅在确认 completed 时退出 0;原生 failed/cancelled 退出 1。提交前已知失败(例如 Provider 或模型不可用)返回 state: null、submission: "not_submitted" 与错误,退出 1;仅在已尝试提交执行且缺少完成证据时返回 unknown,退出 2。SIGINT/SIGTERM 分别退出 130/143:已尝试执行时结果保持 unknown,尚未尝试时可为 not_submitted,均不证明原生任务已取消。

status 查询成功时退出 0,仍须检查返回的状态;null 表示缺少记录,不能据此认定从未执行。原有 status --node-id <设备 UUID> --run-id <原执行 UUID> 仍可使用。

CLI 不自动重派 prompt、工具或审批决定,也不自动 release 或 forgetSession。产物保留在设备端,按 SDK 的 collect 与 release 流程验收、收存和释放;恢复规则见执行生命周期。

真实 Codex 验证结论与范围

2026-10-30,本机源码 CLI(提交 70918bc)已验证一次真实执行、原生审批送达和按原 receipt 查回完整结果;该记录不代替跨机器、独立发行包、全部工具或生产部署验收。查看原始验收结论与完整范围。

CLI 中断后按 receipt 查回原执行

同日本机源码验证(提交 4fcd3b8)确认:仅用 SIGINT 中断 CLI 观察、Server 与 Daemon 持续运行时,可按原 receipt 从 running 查回 completed,未重复提交。查看 SIGINT 验收记录及未覆盖范围。

CLI 被 SIGKILL 后按 receipt 查回原执行

同日本机源码验证(验证时提交 7a51fd1)确认:仅强杀 CLI、Server 与 Daemon 持续运行时,原 receipt 保持完整,可查回原执行的完成结果,未重复提交;这不证明服务重启或真实网络故障恢复。查看 SIGKILL 验收记录及完整范围。

续接、断线和恢复#

相同 runId 与相同输入返回原结果,冲突输入拒绝。sessionKey 与原生会话身份分开;续接须显式传入原生 resumeSessionId 和需要的 previousRunId,不能自动新建替代会话。

SDK 只恢复连接和观察,不自动重发 prompt、工具或审批。断线后先按原 runId 查询 getExecution/events,再按 resultQuery 能力显式 reconcile。null 不证明未执行;缺失完成证据保持 unknown。

Daemon 重启将未完成记录视为 unknown,不恢复进程或旧审批。结果和原生恢复上下文保存在设备端,远端不能指定任意 home、命令或恢复上下文。资源与释放规则见 资源管理。

界舟 · Harness Dock · 原生 Harness 接入与执行管理更新于 2026-11-11