Developer Docs

文档首页

开发文档 · 构建应用

HARP 应用协议

HARP 目标规范、Fellow Host 当前实现范围,以及可安装 HTTP 应用的包与运行约定。

状态:Experimental(Runtime / Host)· 目标规范:Draft — 本文主体是 HARP 目标规范,不代表 Fellow Host 已满足全部条款。当前实现支持 runtime.kind: http 的导入、导出、安装、启动、停止、升级、卸载和 data/ 持久化;尚未完整实现 validate / logs Host API、start.platforms、Manifest 网络权限强制和非 HTTP Runtime。当前产品边界见 Applications

与智能对话互补

智能对话擅长探索、编排与一次性任务:用户描述意图,Agent 调用 Skills 与 MCP,交付报告、清单或写回系统。

但很多业务价值需要关掉聊天窗口仍然存在的软件——固定界面、长期运行、可独立迭代。传统路径是人工把对话里试通的流程再开发成 App;HARP 让非对话式软件的开发方式发生本质变化:智能体直接编写和维护可安装包,宿主隔离运行,用户长期拥有。

智能对话HARP 应用
主要形态会话中的任务与交付独立安装的软件产品
生命周期随会话推进(成果可归档)安装后持续运行
开发方式Skill + MCP 编排智能体产出 harp.json 应用包
典型场景探索、变体多、复杂一次性任务高频固定流程、专用 UI、需本地常驻

Fellow 的特色在于两者同屏共存:在对话里验证流程,再固化为 HARP 应用进入「我的应用」——不是二选一,而是对话负责创造与编排,应用负责持久与专用

软件厂商通过 伙伴合作 发布 HARP 包;以下为协议正文。


1. 一句话定义

HARP 应用是一份可安装、可长期运行、面向人类用户的便携包。智能体负责编写和维护它,宿主负责隔离执行,用户在对话结束后仍然拥有它。

它交付的是产品,不是一次工具调用,也不是一段提示词。


2. 要解决的问题

智能体已经能写代码、起服务、改仓库。缺的是一层跨产品可移植的应用协议

  • 换一个宿主,同一份应用无法安装或启动。
  • 智能体不知道「什么叫安装成功」,只能靠产品私有工具。
  • 用户数据、网络、密钥的隔离没有共同语言,宿主无法互认安全声明。
  • MCP、Skill、MCP Apps、A2A 各自覆盖了工具、流程、对话内 UI、智能体互联,没有人覆盖「装到用户机器上长期跑的应用」

HARP 补的就是这一层。它不取代上述标准,只与它们正交。


3. 定位:和相邻标准的边界

标准回答的问题生命周期主消费者
Agent Skills智能体怎么做这件事读进上下文即结束智能体
MCP智能体可以调用什么能力一次连接智能体
MCP Apps对话里怎么嵌一块 UI一轮对话聊天窗口中的用户
A2A智能体如何发现并调用另一个智能体一次会话或任务另一个智能体
Agent Applications智能体如何用 CLI 操作一个有状态包一次命令智能体
HARP用户留下什么可安装、可独立使用的产品安装后独立存活用户,其次才是智能体

硬规则:

  • HARP MUST NOT 把应用定义成 MCP server。应用跑起来之后 MAY 再暴露 MCP。
  • HARP MUST NOTSKILL.md 当作运行时。包内 MAY 附带 Skill,那是给智能体的作者指南。
  • HARP MUST NOT 把对话内 widget 当作已安装应用。同一产品 MAY 同时提供 HARP 包和 MCP Apps 界面。
  • HARP MUST NOT 要求应用本身是一个智能体。只有当应用要被其他智能体当对等方调用时,才 MAY 附带 A2A Agent Card。

4. 设计原则

  1. 原语极小。 v1 只锁定身份、启动、就绪、数据目录四件事。预留名列表不是 v1 约定。其余全部协商或扩展。
  2. 三个角色,三份约定。 Package / Runtime / Host 分开,禁止揉进一个单体清单。
  3. 声明能力,不规定实现。 规范不写死语言、框架、打包器、操作系统。
  4. 用户数据默认私有。 可变状态有且只有一个宿主注入的数据目录。
  5. 安全是核心,不是附录。 权限必须由宿主强制,不能只靠应用自觉。
  6. 渐进披露。 列表只看身份;安装看完整清单;排障再看日志和健康细节。这是 Skill 能规模化的同一原因。
  7. 扩展用命名空间,不靠 fork。 产品私有字段走 x-<vendor>.*
  8. 一致性测试即规范。 没有可运行的 fixture 和测试,就还只是文档。

5. 角色

角色定义
Author编写包的人或智能体。产出符合 Package Format 的目录。
Package一个目录(或由其得到的不可变制品)。根上有清单文件。
Host安装、隔离、启动、停止、升级应用的运行环境(桌面、云、浏览器运行时等)。
User拥有已安装应用及其数据的人。
Agent代表用户编写、安装、排障应用的智能体。它是 Author 的一种,不是第四种运行时。

宿主 MUST 实现 Runtime ABI。
宿主 MAY 用私有 API 暴露 install/start/stop。那部分在 v1 是建议,不是强制互操作范围。

6. 架构:三层约定

v1 的互操作承诺只有 Layer 1 + Layer 2。
两个兼容宿主拿到同一份包,MUST 能:校验清单、注入 ABI、拉起进程、用健康检查判定就绪、把状态限制在数据目录。
预留 kind、operations 与 Host API 传输不在这份承诺里。

7. Layer 1 — Package Format

7.1 包是一个目录

一个 HARP 包 MUST 是一个目录,根上 MUST 有且仅有一份主清单:

harp.json

MUST NOT 依赖清单之外的隐式入口(例如「没有清单就跑 package.json 的 start」)。
MAY 另含任意实现文件。规范不解释这些文件。

建议布局(信息性,非强制):

my-notes/
  harp.json
  README.md
  src/ 或 server.mjs
  assets/

分发时,包 MAY 被打成 zip / tarball / OCI image。解包后 MUST 仍满足「根上有 harp.json」。
容器化是一种分发与隔离策略,不是第二种应用模型。

7.2 清单:最小合法集

{
  "schemaVersion": 1,
  "id": "notes",
  "name": "Notes",
  "version": "1.0.0",
  "description": "Personal notes. Install when the user wants a lasting notes app.",
  "runtime": {
    "kind": "http"
  },
  "start": {
    "argv": ["node", "server.mjs"]
  }
}

带可选字段的完整 v1 形态:

{
  "schemaVersion": 1,
  "id": "notes",
  "name": "Notes",
  "version": "1.0.0",
  "description": "Personal notes. Install when the user wants a lasting notes app.",
  "license": "MIT",
  "runtime": {
    "kind": "http"
  },
  "start": {
    "argv": ["node", "server.mjs"]
  },
  "health": {
    "path": "/health",
    "timeoutMs": 20000
  },
  "entrypoints": {
    "ui": {
      "kind": "http",
      "path": "/"
    }
  },
  "permissions": {
    "network": "loopback",
    "filesystem": "data-only"
  },
  "x-fellow": {
    "comment": "vendor extension; hosts that do not understand it MUST ignore"
  }
}

7.3 字段规范

未知顶层字段:不带 x- 前缀的 MUST 被拒绝(strict)。
x-<vendor> 对象 MUST 被不认识它的宿主忽略。

字段必填约束
schemaVersion整数。v1 清单 MUST1
id1–64 字符;[a-z0-9]-;不能以 - 开头或结尾
name1–80 字符,给人看的短名
version1–40 字符。SHOULD 使用 SemVer
description否,但强烈建议1–1024 字符。SHOULD 同时写清做什么、何时安装或打开
licenseSPDX id,或指向包内许可证文件的相对路径
runtime.kindv1 MUSThttp。预留名见 7.4,ABI 大纲见 预留约定
start.argv1–32 项;每项 1–500 字符。argv[0] 的两种形态见 7.5
start.platforms按平台覆盖 argv,见 7.5.3。未实现的宿主遇到该字段 MUST 拒绝
health.path/ 开头,最长 200。缺省为 /
health.timeoutMs1000–120000。缺省由宿主决定,SHOULD 为 20000
entrypoints见第 10 节。v1 schema 只接受 ui.kind=http
permissions见第 9 节。缺省见 9.3

id宿主本地标识,不是跨作者、跨宿主的全球身份。

  • 同一宿主上相同 id MUST 视为升级:替换包体,保留 APP_DATA_DIR
  • id MUST NOT 在升级时改变。
  • id MUST NOT 编码路径、用户或宿主名。安装位置由宿主决定。
  • 两个作者都可以使用 notes。宿主 MUST NOT 把它们当成两份并列安装;后装入的覆盖先装的包体。
  • 跨宿主互认(发布者、内容摘要、签名来源)是 v1 非目标。

start.argv 中除 argv[0] 按 7.5 解析外,其余看起来像路径的参数 MUST 是相对包根的路径,MUST NOT 为绝对路径,MUST NOT 逃出包根。

7.4 runtime.kind

runtime.kind 是注册名,不是自由文本。宿主 MUST 按下表分类,MUST NOT 猜测。

分类错误
httpv1 强制继续校验并启动
下列预留名已知但未实现unsupported(C15),除非能力集声明了该 kind
其他任何字符串非法illegal(C03)

v1 JSON Schema 只接受 http。预留名列表的存在,是为了区分「不支持」和「清单非法」,不是为了让 schema 放行。

v1 强制实现(自称 HARP v1 Host 的实现必须能跑):

kind状态含义
httpv1 强制拉起进程;听 HOST/PORT;用 HTTP 健康检查判定就绪

v1 预留(名字和意图冻结,ABI 大纲见 预留约定。v1 宿主 MUST 拒绝,除非它在能力集里声明了该 kind):

kind状态意图
http-static预留无进程。宿主把包内静态文件托管到分配的 HOST/PORT
window预留原生窗口进程(Qt / SwiftUI / WinUI / Flutter 等)。不依赖 HTTP
headless预留长期后台进程,无人类 UI,也不听 HTTP
oneshot预留一次性任务(CLI / 批处理)。退出码即结果
container预留包指向 OCI 镜像或内嵌 image layout;隔离由容器运行时承担
wasm预留Wasm 组件。启动器不是 OS 进程

产品私有运行时 MUST NOT 占用上表名字,也 MUST NOT 自造核心 kind。只允许 x-<vendor> 扩展字段,不能发明 runtime.kind

7.5 启动器:宿主运行时与包内二进制

start.argv[0] 只有两种合法形态。这样任意语言都可以做应用:要么用宿主提供的解释器,要么把编译好的可执行文件打进包。

7.5.1 宿主运行时名

argv[0] 不含 /\,也不以 . 开头,它是逻辑运行时名nodepythondenobun)。

  • 宿主 MUST 解析为自己声明提供的运行时。
  • 解析失败 MUST 拒绝启动。
  • MUST NOT 退回去执行系统 PATH 上的任意同名二进制,除非能力集显式声明 unmanaged-path
  • 其余 argv 按原样传给该运行时。
"start": { "argv": ["python", "server.py"] }

7.5.2 包内可执行文件

argv[0]./ 开头,或含有 / / \,它是相对包根的可执行文件

  • MUST 相对包根;MUST NOT 为绝对路径;MUST NOT.. 段。
  • 宿主 MUST 在启动前确认该文件存在于包内(Windows 上若写的是 ./bin/server 而只有 server.exe,宿主 MAY.exe)。
  • 宿主 MUST 直接执行该文件,MUST NOT 再拿它去匹配宿主运行时名。
  • 其余 argv 传给该二进制。
  • 文件 MUST NOT 是逃出包根的符号链接。
"start": { "argv": ["./bin/notes"] }

这是 Go / Rust / C / C# 单文件分发的标准写法。包作者 MUST 保证该二进制对目标平台可执行;宿主不负责交叉编译。

7.5.3 预留:start.platforms

多平台包 MAYplatforms 覆盖默认 argv。键为平台三元组:linux-x64linux-arm64darwin-x64darwin-arm64win32-x64win32-arm64

"start": {
  "argv": ["./bin/notes"],
  "platforms": {
    "win32-x64": { "argv": ["./bin/notes.exe"] },
    "darwin-arm64": { "argv": ["./bin/notes-darwin-arm64"] }
  }
}
  • 未声明 platforms 时,所有平台都用顶层 argv
  • 声明了 platforms 且当前平台有键:宿主 MUST 用该键的 argv
  • 声明了 platforms 且当前平台无键:宿主 MUST 拒绝启动,MUST NOT 回退到错误架构的二进制。
  • v1 宿主 SHOULD 实现本节;尚未实现时,MUST 在能力集省略 start.platforms,并在清单出现该字段时拒绝(不可静默忽略,否则会跑错架构)。

宿主 SHOULD 注入预留变量 APP_PLATFORM(上列三元组之一),供启动脚本自辨。v1 不强制应用读取它。

7.6 预留 ABI

预留 kind 与 ready 的大纲已移到 预留约定
v1 宿主 MUST 按 7.4 拒绝它们。v1 清单 MUST NOT 包含 ready

7.7 JSON Schema(规范性附录)

本 schema 是 v1 可安装清单 的机械真源。runtime.kind 仅为 httpstart 必填。
预留 kind / ready / operations 不在此列。宿主仍须认识预留 kind 名,以便给出 C15 的 unsupported。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://harp.dev/schema/v1.json",
  "title": "HARP Manifest",
  "type": "object",
  "additionalProperties": false,
  "patternProperties": {
    "^x-[a-z0-9-]+$": true
  },
  "required": ["schemaVersion", "id", "name", "version", "runtime", "start"],
  "properties": {
    "schemaVersion": { "const": 1 },
    "id": {
      "type": "string",
      "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$"
    },
    "name": { "type": "string", "minLength": 1, "maxLength": 80 },
    "version": { "type": "string", "minLength": 1, "maxLength": 40 },
    "description": { "type": "string", "minLength": 1, "maxLength": 1024 },
    "license": { "type": "string", "minLength": 1, "maxLength": 200 },
    "runtime": {
      "type": "object",
      "additionalProperties": false,
      "required": ["kind"],
      "properties": {
        "kind": { "const": "http" }
      }
    },
    "start": {
      "type": "object",
      "additionalProperties": false,
      "required": ["argv"],
      "properties": {
        "argv": {
          "type": "array",
          "minItems": 1,
          "maxItems": 32,
          "items": { "type": "string", "minLength": 1, "maxLength": 500 }
        },
        "platforms": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "linux-x64": { "$ref": "#/$defs/startOverride" },
            "linux-arm64": { "$ref": "#/$defs/startOverride" },
            "darwin-x64": { "$ref": "#/$defs/startOverride" },
            "darwin-arm64": { "$ref": "#/$defs/startOverride" },
            "win32-x64": { "$ref": "#/$defs/startOverride" },
            "win32-arm64": { "$ref": "#/$defs/startOverride" }
          }
        }
      }
    },
    "health": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "path": { "type": "string", "pattern": "^/[^\\s]*$", "maxLength": 200 },
        "timeoutMs": { "type": "integer", "minimum": 1000, "maximum": 120000 }
      }
    },
    "entrypoints": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "ui": {
          "type": "object",
          "additionalProperties": false,
          "required": ["kind"],
          "properties": {
            "kind": { "const": "http" },
            "path": { "type": "string", "pattern": "^/[^\\s]*$", "maxLength": 200 }
          }
        },
        "openapi": {
          "type": "string",
          "pattern": "^[^/].+",
          "maxLength": 200
        },
        "mcp": {
          "type": "object",
          "additionalProperties": false,
          "required": ["path"],
          "properties": {
            "path": { "type": "string", "pattern": "^/[^\\s]*$", "maxLength": 200 }
          }
        }
      }
    },
    "permissions": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "network": { "enum": ["none", "loopback", "allow"] },
        "filesystem": { "enum": ["data-only", "none"] }
      }
    }
  },
  "$defs": {
    "startOverride": {
      "type": "object",
      "additionalProperties": false,
      "required": ["argv"],
      "properties": {
        "argv": {
          "type": "array",
          "minItems": 1,
          "maxItems": 32,
          "items": { "type": "string", "minLength": 1, "maxLength": 500 }
        }
      }
    }
  }
}

8. Layer 2 — Runtime ABI

这是协议的硬核。换宿主还能跑,靠的是这一节,不是清单里多几个字段。

8.1 宿主在启动前 MUST 注入的环境变量

变量何时注入含义
HOSThttp 强制监听地址。v1 MUST 为 loopback(127.0.0.1::1
PORT同上由宿主分配。应用 MUST NOT 自己选端口
APP_ID强制等于清单 id
APP_DATA_DIR强制该应用可写数据目录的绝对路径
APP_CACHE_DIR强制可丢弃缓存目录。宿主 MAY 随时清空
TMPDIR强制临时目录。MUST 落在 APP_DATA_DIRAPP_CACHE_DIR
APP_PLATFORMSHOULD平台三元组,见 7.5.3

http 应用 MUST 从环境变量读 HOST/PORTMUST NOT 把端口写进清单或源码。
预留 kind 的注入规则见 预留约定

为避免和现有产品冲突,宿主 MAY 额外注入自己的别名(例如 VENDOR_APP_DATA_DIR),但 MUST 同时提供上表标准名。应用作者 SHOULD 只读标准名。

8.2 工作目录与文件系统

  • 进程 cwd MUST 是包根(安装后的 bundle 根)。
  • 包目录对应用 MUST 为只读,或在写失败时行为与只读等价。
  • 可变状态 MUST 只写 APP_DATA_DIR(以及 APP_CACHE_DIR / TMPDIR)。
  • 应用 MUST NOT 假设 node_modules、虚拟环境或系统全局包会被宿主拷贝。生产依赖 MUST 打进包,或由声明的 runtime 提供。

8.3 就绪

对 v1 强制的 http

  1. 宿主分配 loopback 端口并启动进程。
  2. 宿主 MUSThttp://$HOST:$PORT{health.path} 做 GET(health.path 缺省为 /)。
  3. 在超时内收到任意 2xx,即视为就绪。
  4. 进程在就绪前退出,MUST 视为启动失败。
  5. 超时仍未就绪,宿主 MUST 停止进程并报失败。

健康检查 MUST NOT 要求鉴权。它不是业务 API。

预留 kind 的就绪判定见 预留约定。v1 宿主在启动前 MUST 拒绝预留或未知的 runtime.kind(C15 / C03),以及清单中的 ready 字段(C02)。

8.4 停止

  1. 宿主 MUST 先发 SIGTERM(Windows 上等价于温和结束请求)。
  2. 宽限期 SHOULD 为 5 秒,MAY 可配置,MUST NOT 超过 30 秒。
  3. 仍未退出则 MUST 强制结束(SIGKILL 或平台等价)。
  4. 应用 SHOULD 在 SIGTERM 后尽快刷盘并退出。

8.5 升级

  • 同一宿主上同一 id 的新版本替换包体。
  • APP_DATA_DIR MUST 保留。
  • APP_CACHE_DIR MAY 被清空。
  • 清单 id 若与已安装身份不一致,MUST 拒绝安装。
  • 破坏性数据迁移是应用自己的责任;协议不规定迁移脚本。

8.6 日志

宿主 SHOULD 把 stdout/stderr 收到应用专属日志槽。
日志格式不标准化。协议只保证:启动失败时,宿主 SHOULD 让 Author/Agent 能读到最近的进程输出。


9. 权限与隔离

权限是声明,由宿主 强制。应用写 "network": "none" 却去连网,宿主 MUST 挡住或启动失败,不能当注释。

9.1 permissions.network

含义
none禁止一切网络,健康检查所需的 loopback 监听除外
loopback只允许回环。这是 v1 缺省
allow允许出站。宿主 SHOULD 在安装或首次启动时取得用户同意

入站在 v1 MUST 仅来自 loopback。把应用暴露到局域网或公网是宿主扩展,MUST 使用 x-<vendor> 并单独授权。

9.2 permissions.filesystem

含义
data-only可写 APP_DATA_DIR / APP_CACHE_DIR / TMPDIR;包只读。缺省
none无持久写。APP_DATA_DIR 可以不存在;写操作 MUST 失败

v1 MUST NOT 提供「读写用户任意文件夹」。那是后续能力,必须带路径声明和用户同意。

9.3 缺省

清单省略 permissions 时,宿主 MUST{ "network": "loopback", "filesystem": "data-only" } 执行。
这是安全默认,不是「未声明就可以为所欲为」。

9.4 密钥

v1 MUST NOT 把密钥写进清单。
需要 API key 的应用 SHOULD 在首次运行向用户采集,并只写入 APP_DATA_DIR
宿主 MAY 用扩展提供密钥注入;未标准化前 MUSTx-<vendor>

9.5 依赖声明(信息性,v1.1 候选)

后续可增加 needs:运行时、系统库、GPU、模型。v1 只要求:宿主解析不了 start.argv[0] 就失败并说明缺了什么。不要在 v1 发明完整的软件包依赖语言。


10. 入口(可选)

entrypoints 声明应用跑起来之后如何被访问,以及每个入口面向谁。

启动不依赖 entrypoints。v1 宿主:

  • MUST 能在忽略 entrypoints 的情况下按 Runtime ABI 把进程跑起来。
  • 仅当它声称「打开人类 UI」时,MUST 把入口理解为 http://$HOST:$PORT{path}path 缺省为 /)。
  • 规范不规定用系统浏览器、内嵌 webview 还是 iframe。

v1 schema 只接受 entrypoints.ui.kind=httpwindow / none 是预留名,见 预留约定;写进 v1 清单会被 schema 拒绝。

10.1 entrypoints.ui(人类界面)

{ "kind": "http", "path": "/" }

10.2 entrypoints.openapi(机器界面)

值为包内相对路径,指向 OpenAPI 3 文档。
智能体 MAY 用它在运行期调用应用。这不是 HARP 的操作约定,SHOULD NOT 整份灌进系统提示。

10.3 entrypoints.mcp

应用进程在某个 HTTP 路径上暴露 MCP。
这是「应用把完整工具能力交给智能体」的挂钩,不是把 HARP 做成 MCP 的子集。

v1 MUST NOT 要求宿主自动把该 MCP 挂进智能体。

10.4 operations

operations / invoke 不是 v1 字段。出现在清单中按未知非 x- 字段拒绝(C02)。
名字与方向见 预留约定

11. 完整示例

包:

notes/
  harp.json
  server.mjs

harp.json

{
  "schemaVersion": 1,
  "id": "notes",
  "name": "Notes",
  "version": "1.0.0",
  "description": "A private notes app. Install when the user wants to keep notes outside the chat.",
  "license": "MIT",
  "runtime": { "kind": "http" },
  "start": { "argv": ["node", "server.mjs"] },
  "health": { "path": "/health" },
  "entrypoints": {
    "ui": { "kind": "http", "path": "/" }
  },
  "permissions": {
    "network": "loopback",
    "filesystem": "data-only"
  }
}

server.mjs 只需遵守 ABI:读 HOST/PORT/APP_DATA_DIR,提供 /health,状态落在数据目录。用什么框架与协议无关。

同一约定下的 Go 二进制(仍是 v1,只要宿主实现了 7.5.2):

{
  "schemaVersion": 1,
  "id": "notes",
  "name": "Notes",
  "version": "1.0.0",
  "runtime": { "kind": "http" },
  "start": {
    "argv": ["./bin/notes"],
    "platforms": {
      "win32-x64": { "argv": ["./bin/notes.exe"] }
    }
  },
  "health": { "path": "/health" }
}

下面不是合法 v1 包:过不了 v1 schema。写在这里只为说明预留名;宿主应报 unsupported(C15),而不是当成可安装清单:

{
  "schemaVersion": 1,
  "id": "sketch",
  "name": "Sketch",
  "version": "1.0.0",
  "runtime": { "kind": "window" },
  "start": { "argv": ["./bin/sketch"] },
  "ready": { "kind": "window" },
  "entrypoints": { "ui": { "kind": "window" } }
}

智能体侧最小闭环:

  1. 在工作区生成上述目录并自测健康检查。
  2. 请宿主 validate,再 install
  3. start 成功后把 URL 交给用户。
  4. 用户关闭对话后,应用仍在;再次打开只需 start,不必重写。

12. 版本与兼容

  • schemaVersion 标识清单约定,不是应用的 version
  • 同一 schemaVersion 内:新加可选字段 MUST 保持旧宿主可忽略或可经 x- 扩展。
  • 删除或改语义的字段 MUSTschemaVersion
  • 宿主 MUST 拒绝更高的 schemaVersionMUST NOT 降级猜测。
  • 应用 version 只服务升级与展示;协议不比较它的大小,除非宿主自己做。
  • v1 schema 是可安装清单的验收契约。预留名列表不在 schema 里;把 operations 或新 kind 写进该契约,必须升文档版本(v1.1 / 新 schemaVersion),不能靠「旧宿主忽略未知字段」——v1 对未知非 x- 字段是拒绝。

能力协商(给 Host API / v2):

{
  "protocol": "harp",
  "schemaVersions": [1],
  "runtimeKinds": ["http"],
  "readyKinds": ["http"],
  "uiKinds": ["http"],
  "executables": ["node", "python"],
  "bundledExecutable": true,
  "startPlatforms": true,
  "permissions": {
    "network": ["none", "loopback", "allow"],
    "filesystem": ["none", "data-only"]
  }
}

应用不需要嵌入这份文件。这是宿主描述自己的方式。


13. 安全考虑

HARP 应用是长期本地进程,攻击面大于 Skill,也大于一次 MCP 调用。v1 把能力收得很窄,是有意的。

宿主 MUST

  • 按第 9 节强制权限,而不是只记录。
  • 把不同 idAPP_DATA_DIR 隔开。
  • 不把用户主目录、其他应用数据、宿主密钥目录挂进可写根。
  • 校验相对路径,禁止 .. 与绝对路径。
  • network: allow 前取得用户同意。

宿主 SHOULD

  • 用平台沙箱(容器、seatbelt、AppContainer、seccomp 等)落实声明。
  • 对安装来源做提示(工作区生成 / 本地路径 / 远程包)。
  • 限制 CPU、内存、磁盘。v1 不规定数字,但裸奔不应是默认。

已知非目标(v1 不管,文档必须写明以免误用):

  • 代码签名与供应链(v1.1+)
  • 跨宿主的包身份(发布者、内容摘要、签名来源)
  • 多用户 ACL
  • 把服务发布到公网
  • 应用间 IPC
  • 自动注入用户的第三方账号

14. 合规

一个实现要自称 HARP v1 HostMUST 通过公开的一致性套件。套件与文字规范冲突时,以套件加 errata 为准——先把套件写进标准治理,避免各家「按自己理解合规」。

v1 套件至少包括:

编号断言
C01接受第 7.2 节最小清单
C02拒绝非法 id、绝对路径、逃逸路径、未知非 x- 字段(含 v1 中的 readyoperations
C03拒绝未列入预留名列表的 runtime.kind(错误为 illegal)
C04注入 HOST/PORT/APP_ID/APP_DATA_DIR
C05应用把文件写到 APP_DATA_DIR 外时失败或不可见
C06健康检查 2xx 后报告就绪
C07超时未就绪则杀掉进程并失败
C08升级保留 APP_DATA_DIR
C09network: none 时出站失败
C10停止路径:温和结束,必要时强制结束
C11忽略 x-* 仍能启动
C12更高 schemaVersion 被拒绝
C13start.argv[0]./rel 时执行包内文件,不解析为宿主运行时名
C14包内路径含 .. 或绝对路径时拒绝
C15预留名列表中的 runtime.kind 被拒绝,且错误为 unsupported,与 C03 的 illegal 可区分

套件 MUST 带一个最小 fixture 应用(约几十行),不依赖特定 Web 框架。

一个包要自称 HARP v1 AppMUST

  • 通过 schema 校验;
  • 在合规宿主上于超时内就绪;
  • 重启后只从 APP_DATA_DIR 恢复可变状态。

15. 治理建议

标准要能离开第一家实现者独立活着,需要:

  1. 规范仓库与产品仓库分开(例如 harp org)。
  2. JSON Schema + 一致性套件作为唯一机械真源;Markdown 是对人的叙述。
  3. 变更流程: 先实现两个独立宿主再升「Final」。一个实现的字段不得进入核心。
  4. 扩展注册表: 记录 x-<vendor> 前缀,避免碰撞,不审核产品语义。
  5. 许可: 规范文本与 schema 用开放许可(建议 CC-BY 4.0 + schema 的 Apache-2.0)。
  6. 命名空间: 正式站点建议 https://harp.dev;清单 $id 与之对齐。在独立域名可用前,草案可用 https://harp.dev/schema/v1.json 作为稳定标识,不必先上线。

第一年成功标准不是「字段多」,而是:两家宿主跑通同一份 fixture,第三家智能体产品能按 schema 生成可安装包。


16. Layer 3 — Host API(v1 信息性)

以下 不是 v1 合规条件。写出来是为了避免每个产品发明一套语义,给 v2 留对齐点。

建议的 v1 动作:

动作输入结果
validate包目录清单 + 权限摘要,不安装
install包目录id 安装;同 id 视为该宿主上的升级并保留数据
list身份字段 + 运行状态
startid就绪后的本地 URL
stopid进程结束
uninstallid删除包;SHOULD 询问是否删除 APP_DATA_DIR
logsid最近进程输出

v1.1 候选(现在实现也不进入 v1 合规):describeinvoke。见 预留约定

智能体侧的渐进披露:

  1. 系统提示里只放已安装应用的 id / name / description
  2. 需要安装或排障时再读完整 harp.json(仍 MUST NOT 把端口交给模型,除非要把 URL 给用户打开)。
  3. 启动失败再拉 logs

传输(HTTP、JSON-RPC、MCP tool)不在 v1 规定。先冻结包与 ABI,再冻传输。


17. 智能体作为作者时的约定

HARP 的作者经常是模型,不是人类。规范因此要让「最小合法包」小到模型稳定生成,同时让宿主能机械拒绝危险包。

Author(含智能体)MUST

  1. 在包根写下合法的 harp.json(通过 v1 schema)。
  2. 监听 HOSTPORT,并提供可匿名 GET 的健康路径。
  3. 只把可变状态写到 APP_DATA_DIR
  4. 使用宿主运行时,或把可执行文件放进包并用 ./ 相对路径启动。
  5. 不把密钥、绝对路径、用户主目录写进清单。
  6. 不填写预留 runtime.kind(生成器只引用第 7.2 节最小合法集)。

Author SHOULD

  1. description:做什么 + 何时安装/打开。
  2. 提供 entrypoints.uikind=http)。
  3. 用 SemVer。
  4. 在包内附 README.md 给人类;需要时附 SKILL.md 作为作者指南。

宿主对智能体 MUST

  1. 用 schema 校验,失败时返回机器可读错误(字段路径 + 原因)。
  2. 拒绝逃逸包根的 start.argv 路径。
  3. 拒绝符号链接逃逸(若分发形态是目录拷贝)。
  4. 不在用户明确要求前卸载应用。
  5. 对预留 runtime.kind 与未知 runtime.kind 给出可区分的错误(C15 / C03)。

预留,不是 v1

本节冻结名字与方向,方便以后补 ABI,并避免厂商抢注。
不是 v1 互操作范围。v1 一致性套件不覆盖本节的字段形态。

v1 JSON Schema(见 Package Format)只接受 httphealth、可选的 entrypoints.ui.kind=http,以及 permissions
因此:

清单里出现v1 宿主的分类套件
下表预留 runtime.kind已知但未实现 → 拒绝,错误为 unsupportedC15
未列入列表的 runtime.kind非法 → 拒绝,错误为 illegalC03
readyoperations,或其他未收入 v1 schema 的非 x- 字段未知字段 → 拒绝C02

在这些名字进入强制范围并写入 schema 之前,细节可以补字段,但不能改已公布的含义。
智能体生成 v1 包时 SHOULD NOT 阅读或引用本节。


18. 预留 ABI 大纲

http-static

  • start
  • 宿主把包内目录托管到分配的 HOST/PORT
  • 就绪:对该 origin 的 health.path/ 返回 2xx。
  • 可写状态仍只有 APP_DATA_DIR(若静态应用需要存储,SHOULD 另配一个小的 http,而不是让静态托管写文件)。

window

  • start 必填,解析规则同 Package Format 7.5。
  • MUST NOT 要求应用听 HTTP。宿主 MUST NOT 因未绑定 PORT 而判失败。
  • HOST/PORT MAY 仍注入,供应用内嵌本地服务;应用可忽略。
  • 就绪默认预留 ready.kind=window:进程已创建且出现至少一个属于该进程的可见窗口。宿主无法检测窗口时,MUST 拒绝该 kind,不能改用「进程还活着」凑合。
  • entrypoints.ui.kind SHOULDwindow。用户入口是该窗口,不是 URL。

headless

  • start 必填。
  • 无人类 UI。entrypoints.ui SHOULD{ "kind": "none" }
  • 就绪预留 ready.kind=notify:进程在超时前向 $APP_DATA_DIR/ready 写入一行(建议 ISO-8601 时间戳)并关闭文件。宿主看到该文件出现即就绪。
  • 也可声明 ready.kind=httptcp,若进程选择听端口。

oneshot

  • start 必填。
  • 就绪即进程退出。ready.kind 缺省为 exit-zero:退出码 0 成功,非 0 失败。
  • MUST NOT 当长期会话持有。Host API 的 start 在这里语义是「运行一次并返回结果」。
  • 用于批处理、导出、索引,不是常驻 App。

container / wasm

  • start.argv 不适用或另有字段(镜像引用、component 路径)。具体字段在该 kind 升强制时再冻。
  • 数据目录、权限声明、身份字段仍然适用。
  • v1 MUST NOThttp 解释成「其实可以是容器」。

预留 ready

health 是 v1 http 的就绪声明。通用形态预留为:

"ready": {
  "kind": "http",
  "path": "/health",
  "timeoutMs": 20000
}
ready.kind状态判定
httpv1 已实施(经 healthGET http://$HOST:$PORT{path} 返回 2xx
tcp预留能连上 $HOST:$PORT 即可,不要求 HTTP
exit-zero预留进程以状态 0 退出
notify预留$APP_DATA_DIR/ready 出现
window预留属于该进程的可见窗口出现

方向(在该字段进入 schema 之前不构成 v1 合规):

  • 若同时写 healthreadyready 优先。
  • ready.kindruntime.kind 不匹配(例如 http + window)时拒绝。
  • 不认识的 ready.kind 必须拒绝,不能当成「进程已启动即就绪」。

v1 清单 MUST NOT 包含 ready。写了即按未知字段拒绝。


19. 预留:operations(对话操作)

用户打开 UI,智能体在对话里操作同一份应用状态。这是第二条消费者,不是第二种应用。

v1 operations 收入 schema,也 要求宿主实现 invoke
下列名字予以占位,避免各家用浏览器刮页或把整份 OpenAPI 塞进系统提示。字段形态在进入 v1.1 schema 前可以补,不能改已写含义。

预留名:operationsinvokebind.kind = http | mcp | cliAPP_INVOKE_TOKEN

方向:

  1. 状态仍在应用里。对话记忆不是源。
  2. 智能体只调用声明过的操作。
  3. 宿主代发,智能体不直连端口。
  4. 列表只给 id 与一句话;调用前再加载 schema。
  5. 破坏性操作要确认。

示意(不是合法 v1 清单):

"operations": [
  {
    "id": "notes.add",
    "description": "Add a note. Use when the user wants to save text into Notes.",
    "confirmation": false,
    "inputSchema": {
      "type": "object",
      "additionalProperties": false,
      "required": ["text"],
      "properties": { "text": { "type": "string", "minLength": 1 } }
    },
    "bind": {
      "kind": "http",
      "method": "POST",
      "path": "/api/notes"
    }
  }
]
名字意图
operations[].id应用内稳定名,对话引用它而不是 URL
description做什么 + 何时调用
confirmation为真时先取得用户同意
inputSchema代发前校验
bind宿主如何执行;不交给模型

未进入 v1 的应用仍然完整:用户打开 UI,智能体 start / stop / 读描述。浏览器刮页不是 HARP 约定。

20. 路线

阶段做什么不做什么
v1 DraftPackage + Runtime ABI + 包内二进制 + 只接受 http 的 schema + 预留名列表(不进 schema)+ 合规表把预留 kind / operations 写进 v1 schema;Host API 传输
v1 Final两个独立宿主通过 C01–C15;fixture 含解释器与包内二进制各一份把预留 kind 升为强制
v1.1operations + 宿主 invoke(可先只实现 bind.http);start.platforms 强制化公网暴露;自动把整个 MCP 挂进对话
v2Host API 传输;window / oneshotbind.mcp / bind.cli把 MCP 或 Skill 收编为基座

刻意推迟的东西:

  • 应用商店与发现索引
  • 代码签名、SBOM、可重现构建
  • GPU / 本地模型声明
  • 应用之间的官方 IPC
  • 计费与授权许可执行

这些都可以在核心稳定后以扩展或后续版本出现。过早写进 v1,标准会变成某一家运行时的说明书。


21. 决策记录

决策选择理由
协议名HARP / 宿主应用运行时协议对准「包 ↔ 宿主运行时」;避开 Agent Application(s) 与 MCP Apps
主文件名harp.json与协议同名,可校验;不和 PWA manifest.jsonAPP.md 抢名
v1 唯一强制 runtimehttp先冻一种可测的原语;其他 kind 只占名
语言与二进制运行时名 ./ 包内文件任意能起进程的语言都合法,不必等新 kind
非 Web预留 kind / ready / ui名字冻结,避免厂商抢注;v1 不强制实现
不把 Host API 放入 v1 强制范围只冻包和 ABISkill 靠文件约定成功;MCP 级 RPC 等有第二个实现再冻
默认网络loopback本地产品够用;出站必须用户同意
默认文件系统data-only长期进程的最低可信承诺
健康检查匿名 HTTP GET不发明新的就绪协议
扩展x-<vendor>产品差异不污染核心
与 MCP 的关系可选入口,不是基座应用的主消费者是用户,不是工具调用方
对话操作v1.1 再收入 schema;v1 只占名避免模型把 operations 当成最小合法集
id 语义宿主本地标识不假装全球唯一;跨宿主互认留到签名/摘要

22. 下一步(若要将草案推进行业)

  1. 把第 7.7 节 schema 与第 14 节用例抽成独立仓库 harp-spec
  2. 实现官方 fixture 应用与宿主侧测试夹具(进程隔离可用假宿主)。
  3. 写一份不超过两页的 Authoring Guide for Agents,专供系统提示引用,且只引用第 7.2 节最小合法集。
  4. 找第二个宿主按本文实现,冻结 v1 Final。
  5. 在规范仓库用 errata 而不是产品发行说明演进语义。

在第二家实现出现之前,本文应保持 Draft。强制范围只减不增;预留名列表可以补 ABI 细节,但已公布的 kind / ready / ui 名字不得改义或挪作他用。