diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 00000000..fc7587a4 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,471 @@ +# 公开 API 文档实施计划 + +本计划把已核对的 DesireCore 接口整理成公开站点中的“集成与 API”区域。用户应能根据文档取得正确的服务地址和授权凭据,完成一次调用,理解响应与运行结果,并处理实际错误。实施采用主 Agent 协调、多个 subAgent 按接口能力并行核查、编写和验证的方式;先交付一个完整可用的接入流程,再按各项能力的实际进度发布扩展参考和 OpenAPI 下载。 + +状态:P0—P5 完整选定范围已实施并通过本地验收;8 组双语页面、示例客户端、24 个端点及三个服务的规范就绪。上游 #4793 与源码 #4798 已全绿合并;公开文档 PR #72 正在刷新最终来源与发布。P6 自动部署与生产验收进行中。更新日期:2026-10-15。 + +## 1. 实施目标与完成标准 + +- 公开文档明确说明接口用途、支持的运行形态、可执行的接入步骤、权限和兼容范围。 +- 每个公开端点都能追溯到实际注册路由、请求/响应契约、固定代码版本和发行依据。 +- 首批提供至少一个从配置、调用到结果观察的完整流程。Agent Webhook 为首选试点;其配置和结果观察出现缺口时,其它已经就绪的完整流程可先进入交付批次,Webhook 的已确认事实继续供后续实施使用。 +- 中文与英文页面一起交付,接口路径、字段、权限、版本和错误处理一致。 +- 文档、示例、导航、两种语言构建和发布后的线上链接验证通过。 +- 后续机器可读参考只包含经过筛选、验证且已公开的端点,覆盖范围在下载页明确说明。 + +本轮规划覆盖公开站点、内部 API 文档的必要校准,以及确实阻碍公开接入的产品或代码契约补齐。代码补齐以具体失败场景为依据,独立于文档 PR 交付;不因编写文档而重写全量服务、统一所有错误壳或建立新的权限体系。 + +## 2. 核查基线与已确认事实 + +| 来源 | 固定基线 | 用途 | +|---|---|---| +| `desirecore` | `origin/dev`:`6eb08c32676d477bff2844f4ac0d6fd40013e54f` | 当前实现的核查基线;实施时通过 `git show :` 或隔离 checkout 读取 | +| `desirecore-docs` | `main`:`40ea7809113dc66d35074c26e55c2d4bd7307d70` | 内部接口说明、模块合同和设计资料 | +| 公开 `docs` | `main`:`34fb4a7cbc8bd2f453ee79536981d22c7448373b` | 现有文档、站点结构和发布流程 | + +以上是初步核查基线,逐端点的完整盘点属于 P0。实施开始时刷新相关远端,并记录新基线及其相对上述基线的接口差异。`dev` 的代码事实、正式客户端的支持范围和一次调用的验证结果分别记录,不能互相替代。 + +已确认的事实与对应实施要求: + +| 事实 | 实施要求 | +|---|---| +| 内部 `技术/API/HTTP/索引.md` 收录多模块端点、开发候选和内部协议 | 逐项筛选后再进入公开清单;保留内部总索引的工程用途 | +| `技术/API/对外接口设计.md` 的 Companion `/agents/{id}/run`、SSE `/stream` 与当前对话入口不同 | 校准其设计状态和实际协议入口,公开示例使用已注册路径 | +| 对话提交使用 `/api/query/start`、`/api/query/follow-up`,运行消息经 Socket.IO 回流 | 对话文档必须覆盖 HTTP 提交与 Socket.IO 关联;`202` 仅表示接纳,不能等同持久提交或完成 | +| Agent Service、Workflow Service、Compat Proxy 的地址和认证合同不同 | 为各服务分别核对地址、凭据、权限和结果传输方式 | +| 桌面实例端口动态分配,主实例凭据可能只在内存中持有 | 示例不能猜固定端口、教用户读取不存在的 token 文件或截取 Renderer 内部凭据 | +| `/health` 用于探活,`/api/client-handshake` 仍受实例认证 | 探活、认证验证、凭据签发和用户登录分别说明;握手不发放访问凭据 | +| Agent Webhook 已注册,立即返回 `200` 和 `{accepted:true, agentId, event}` | 说明后台执行语义;响应不包含 Run ID,也不证明运行完成 | +| Compat Proxy 注册 `/v1/messages`、`/v1/chat/completions`、`/v1/models` | 只承诺已注册且经验证的协议;供应商出站支持 Responses 等能力不代表代理提供同名入站端点 | +| Workflow Service 有独立认证,部分执行入口另要求执行授权 | service token 不自动等于所有运行操作的授权;逐端点核对现行准入 | +| 现有站点基于 Docusaurus 3.9.2,中文默认、英文 `/en/` | 首批复用 Markdown/MDX 和独立 sidebar,按实际构建验证路由 | +| `static/api/` 是被忽略的更新日志 JSON 生成目录 | 后续 OpenAPI 文件放入 `static/openapi/`,不复用更新日志插件 | + +## 首批本地实施结果(2026-10-13 历史) + +本轮固定源码为 `2bb7add4497c6068a977b248106e922f9dfb196b`,内部文档更新到 `0b84089da1f7c050ccdab6f2912b36a8fcdb8802`;公开站点的变更前基线仍为上表记录。主 Agent负责共享入口和最终验证,三个 subAgent分别完成接入/排错、Webhook、定向源码验证与文档校验器,并交叉审阅。 + +本地已交付 4 组中英文页面、独立 sidebar、首页/更多/集成概览及导航入口、端点清单、维护说明与离线检查脚本。`pnpm typecheck`、双语 `pnpm build`、源码的 111 项定向测试、实际 JSON 示例 Schema 和本地页面/元信息检查通过。证据及未覆盖范围见 [api/verification.md](./api/verification.md)。 + +| 原任务 | 本轮结果 | +|---|---| +| T0.2—T0.4 | 首批路由/认证/Schema/配置与结果合同已核对;未做完整内部端点盘点,组合任务保持未勾选 | +| T0.5 | 代码基线与发行线索分别记录;没有证明首次发行版本,也没有新增客户端版本约束 | +| T0.6 | 公开页校准实际响应;内部原文仍有旧示例,后续单独校准 | +| T1.4 | 本批不交付桌面配对客户端参考,未将其宣传为最小权限或重启撤销 | +| T2.1、T2.4 | 表单缺少可信运行权限配置的生成流程,从零自助配置快速开始尚未完成;本批限定管理员预配置事件调用 | +| T2.5 | 接收反馈与主动交付合同已核对,真实模型和主动交付未验收;无通用请求级结果读取 | +| T2.7 | HTTP 认证/错误边界和模拟后台失败通过;查询/存储替身范围明确,不等于完整模型运行 | +| T5.7 | 已有轻量清单/双语/JSON/构建检查;OpenAPI 尚未交付,完整一致性任务保持待办 | +| T6.2—T6.7 | 尚未发布,不宣称自动部署或线上检查完成 | + +这次收窄的是首批 Webhook 的公开承诺:已配置事件的调用参考可以完成,尚未开放的自助配置直接说明暂不可用。无需等待这一产品缺口来发布成立的接入和请求合同。 + +## 3. 公开范围与发布条件 + +### 3.1 接口分组 + +| 分组 | 候选内容 | 默认实施顺序 | +|---|---|---| +| 接入基础 | 服务地址、部署形态、授权、探活、认证握手、错误排查 | P1;只给有实际接入途径的示例 | +| 外部事件触发 | `POST /api/webhook/:agent_id` | P2 首选试点,先验证配置、权限和结果观察 | +| 只读实例数据 | 已验证的 Agent 列表、Run 状态、历史/回执读取 | P3 按场景筛选,逐项说明数据归属和可见性 | +| 对话执行 | query start/follow-up 与必要 Socket.IO 事件 | P3;必须形成一次完整调用流程 | +| 模型协议代理 | Compat Proxy 的 Messages、Chat Completions、Models | P4;先验证外部调用方的地址与凭据获取流程 | +| 工作流集成 | Workflow Service 触发、执行、进度和结果 | P4;先核对独立凭据、执行授权和定义引用 | +| 管理与宿主内部协议 | 密钥读取/管理、支付、内部回调、实例控制、Renderer 专属操作等 | 维持内部参考;只有存在明确外部使用场景且通过发布条件时才单独评估 | +| 规划或退役接口 | 设计页中的目标端点、开发候选、返回 410 的旧入口 | 不生成可调用示例;仅在确有迁移需要时提供替代说明 | + +已有 MCP、Skill、供应商配置指南继续承担 DesireCore 连接外部能力的说明。新 API 概览通过链接引导读者选择接入路线,避免重复维护这些指南。 + +### 3.2 每个端点的发布条件 + +一个端点进入公开参考前必须同时满足: + +1. 已确认方法、路径和实际注册的 listener;宿主/部署能力条件明确。 +2. 目标读者能通过文档中的操作取得地址与所需凭据,完成身份验证。 +3. 权限、资源归属、审批和额外治理要求可被调用方理解并遵守。 +4. 请求示例通过真实 Schema;响应、错误和异步语义来自对应代码分支。 +5. 示例有完成判定或明确的结果观察方法;有限能力按真实支持范围描述。 +6. 发行支持范围已核对。客户端有效版本、协议编号和握手兼容字段分别保留含义。 +7. 至少一个与公开支持形态一致的完整调用经过验证,结果可追溯。 +8. 中英文正文和维护清单一致,文档构建与链接验证通过。 + +若仅某个能力未通过条件,记录具体缺口并继续实施其余已成立内容。不得用隐藏的 Renderer 标记、复制内存凭据或修改敏感配置的临时方式,把失败的外部流程写成快速开始。 + +## 4. 信息结构与文件落点 + +首批以独立 sidebar 承载“集成与 API”,从现有“更多”和“集成能力概览”进入。具备一个完整可用流程后再加入顶部导航和首页入口;同时检查现有导航在窄屏下的空间。 + +| 文件 | 稳定 URL | 内容与交付阶段 | +|---|---|---| +| `docs/07-api/index.md` | `/api` | 接入路线、已经公开的能力、适用对象;P1 | +| `docs/07-api/_category_.json` | — | 分类链接使用 `api/index`,避免与 index 页重复生成路由;P1 | +| `docs/07-api/01-connection-and-auth.md` | `/api/connection-and-auth` | 实例地址、部署形态、凭据来源、权限、验证连接;P1 | +| `docs/07-api/02-agent-webhook.md` | `/api/agent-webhook` | 配置到触发再到结果观察的指南和端点参考;P2 | +| `docs/07-api/03-errors-and-troubleshooting.md` | `/api/errors-and-troubleshooting` | 实际错误结构、接收/完成区别、用户可操作的排查步骤;P1/P2 | +| `docs/07-api/04-conversations-and-runs.md` | `/api/conversations-and-runs` | HTTP + Socket.IO 调用闭环、身份、重试和恢复;P3 | +| `docs/07-api/05-model-proxy.md` | `/api/model-proxy` | 单列模型代理地址、凭据与协议子集;P4 | +| `docs/07-api/06-workflow-integration.md` | `/api/workflow-integration` | 独立服务接入、执行授权、进度和结果;P4 | +| `docs/07-api/07-openapi.md` | `/api/openapi` | 机器参考下载、覆盖范围与使用说明;P5 | +| `i18n/en/docusaurus-plugin-content-docs/current/07-api/` | `/en/api/...` | 镜像已发布的中文页面,文件名与 slug 相同 | +| `api/endpoint-catalog.json`、`api/README.md` | 不进入站点文档路由 | 公开端点清单和维护约定;P0 起维护 | +| `static/openapi/agent-service.json` | `/openapi/agent-service.json` | 经过筛选的 Agent Service OpenAPI 下载;P5 | + +方法、地址、认证和参数合同确认后,即可在工作分支编写页面并验证示例;无需等待全量端点盘点或其它服务验收。依据尚未成立的草稿留在文档路由之外。尚未通过发布条件的内容不合入发布分支、不加入公开入口;主 Agent 在每个交付批次只集成已经就绪的页面。新页面使用显式 slug;文件编号和标题调整不改变 URL。页间链接优先使用 Markdown 文件链接,让 Docusaurus 检查目标。 + +配套修改落点: + +- `sidebars.ts`:增加 `api` sidebar,目录为 `07-api`。 +- `docs/05-more/index.md` 及英文镜像:增加 API 概览入口。 +- `docs/02-user-guide/09-capabilities/06-integrations-overview.md` 及英文镜像:增加从外部调用 DesireCore 的使用路线。 +- `docusaurus.config.ts`:完整流程就绪后增加 navbar 与资源 footer 入口。 +- `docs/index.md` 及英文镜像:增加首页入口,沿用现有布局。 +- `i18n/en/docusaurus-theme-classic/navbar.json`、`footer.json`、`i18n/en/docusaurus-plugin-content-docs/current.json`:按实际新增的导航翻译,审阅生成差异。 +- `README.md`:添加 API 内容维护入口;沿用现有版本与用户写作规范。若修改规范本身,同步 `AGENTS.md`。 + +公开文档网站的 `/api` 是阅读地址。所有调用示例另写实例 `BASE_URL`,不得让读者把 `https://docs.desirecore.com/api/...` 当成业务请求地址。 + +## 5. 来源清单与维护方式 + +完整内部盘点建议放在 `desirecore-docs/技术/API/公开接口核查台账.md`,保留内部候选、退役项和未解决问题。公开仓库的 `api/endpoint-catalog.json` 只保留实际选中的端点和可公开的维护信息。 + +公开清单的最小字段: + +| 字段 | 内容 | +|---|---| +| `operationId`、`service`、`method`、`path` | 唯一操作名、所属服务、实际方法与路径 | +| `doc`、`slug` | 中文来源文件与稳定文档 URL;英文镜像按约定定位 | +| `deploymentModes`、`authentication`、`permissions` | 支持形态、所需凭据和额外权限 | +| `resultDelivery` | 同步响应、异步观察、Socket.IO 或 SSE 等真实结果方式 | +| `source` | 来源仓库、相对源码路径、符号和固定 commit SHA;行号仅辅助定位 | +| `schemaRefs` | 已有请求/响应 Schema 引用;没有响应 Schema 时记录确认方式 | +| `effectiveVersion`、`versionEvidence` | 确需版本约束时填写真实版本与依据;无必要时可为空 | +| `verifiedAt`、`verification` | 核查时间、已验证形态和脱敏验证记录 | +| `spec` | 已收录该端点的 OpenAPI 文件;尚未覆盖时为空 | + +清单不承载新的业务合同,不复制全量内部路由。机器可读参数以已有 Schema 为来源,行为以实际处理器为来源,公开 prose 说明用户如何操作。发生来源冲突时先校准,不能通过互相复制把错误固定到多个文件中。 + +可在需要时增加无依赖的 `scripts/check-api-docs.mjs` 和 `api:check` 命令,核查清单唯一性、文档及英文镜像存在性、slug、版本约束和 OpenAPI 覆盖一致性。公开仓库 CI 不依赖私有代码 checkout;代码来源复核在内部工作流执行。避免用简单字符串匹配替代真实路由注册或权限检查。 + +## 6. 分阶段任务与验收 + +### P0 固定来源与完成端点盘点 + +前置条件:无。 + +- [x] **T0.1** 刷新三个仓库的远端,记录主仓、文档仓库和目标发行版本的固定 SHA;查看接口相关差异。 +- [x] **T0.2** 读取 `技术/API/HTTP/索引.md`、各功能模块 API 文档、实时事件和实际路由注册,建立完整内部台账。 +- [x] **T0.3** 为候选端点记录 service/listener、运行形态、鉴权、额外治理、Schema、结果方式、副作用、版本及验证状态。 +- [x] **T0.4** 重点核对 `http-server.ts`、`auth/instance-auth.ts`、`auth/governed-endpoint-auth.ts`、配对认证、query 路由、Webhook、Compat Proxy 和 Workflow Service。 +- [x] **T0.5** 对照可安装的客户端/部署版本,确认已发行能力与候选能力。已有明确版本保留原值,无法确定但确需标注的已可用功能遵循仓库默认版本规则。 +- [x] **T0.6** 校准《对外接口设计》的设计状态,补充 Agent Webhook 的现行内部合同;记录其它影响首批公开内容的差异。 +- [x] **T0.7** 选定首批支持形态和端点,建立公开端点清单;优先核验“部署所有者调用自己的 standalone 实例”和配置了专用 secret 的外部 Webhook。把阻碍接入的产品/代码缺口写成具体任务,浏览器和第三方应用凭据另行核验。 + +产物:内部核查台账、公开端点清单、首批支持矩阵、差异修正任务。 + +验收:首批每个端点均有实际路由、认证和发行依据;所有缺口能对应到具体用户流程,不存在以“接口已注册”代替“外部可用”的判定。当前主仓存在用户本地修改,核查和实现均不得覆盖这些修改。若创建 worktree,必须初始化并拉取最新子模块代码,记录各子模块基线及其与主仓指针的差异。 + +### P1 建立接入基础与站点区域 + +前置条件:固定代码基线、清单格式和目标 slug 约定已确定。连接认证正文依赖对应认证合同确认;站点结构、维护模板和清单校验准备可同时开始。每页发布时满足第 3.2 节条件。 + +- [x] **T1.1** 创建 API 概览、连接认证和错误排查页面及英文镜像。 +- [x] **T1.2** 用实际步骤说明如何获得实例地址、如何取得/保管/撤销对应凭据;按桌面、standalone 和浏览器调用分别说明支持条件。 +- [x] **T1.3** 明确供应商 API Key、实例访问凭据、配对客户端凭据、Webhook secret 和代理安全 token 的不同用途。 +- [x] **T1.4** 核对配对凭据的实际权限与生命周期,不将当前 `interactive` 标记宣传为严格最小权限,也不宣称重启即撤销长期凭据。 +- [x] **T1.5** 提供通过发布条件的只读连接验证示例;说明 `/health` 与已认证 handshake 的作用。无法取得有效凭据的调用形态不提供伪快速开始。 +- [x] **T1.6** 添加独立 sidebar,以及“更多”和集成概览入口;完整试点就绪后再添加首页、navbar 和 footer。 +- [x] **T1.7** 建立端点页面模板与 `api/README.md` 维护约定。 +- [x] **T1.8** 构建两种语言并检查稳定路由、页面链接、语言切换和导航布局。 + +验收:读者能判断自己适用的接入方式;所有示例变量均有真实获取途径;不会混用认证头、服务地址或客户端版本。公开正文只出现用户可理解的条件,来源 SHA、内部 PR 和验收门控留在维护清单。 + +### P2 完成 Agent Webhook 试点 + +前置条件:代码基线已固定,即可并行核查配置、权限和结果观察。Webhook 合同确认后即可编写页面、验证示例;发布依赖 P1 对应的接入说明与已确认的发行支持范围,不等待 P1 全部任务或其它服务完成。 + +- [x] **T2.1** 核对 Webhook 配置的实际入口,包括启用、secret、事件名、prompt 模板和执行权限。确认用户可以通过现有 UI/获准工具操作完成配置,且由可信入口生成执行权限快照;若只能依赖内部写入,先补齐受支持的配置流程,不让读者照抄内部权限对象。 +- [x] **T2.2** 核对请求 `event`、`payload`、模板替换规则、1 MiB 请求体上限、启用状态与目标 Agent 条件。说明第三方原生事件需映射为 DesireCore 的请求结构,不能宣传为直接支持 GitHub 原生 body 或签名认证;`priority` 未参与实际调度时不承诺优先级效果。 +- [x] **T2.3** 说明外部调用使用的 secret 验证方式;桌面回环地址的访问范围和远程部署条件分别给出。 +- [x] **T2.4** 提供一组无真实外部副作用的完整配置与 curl 示例,使用专门测试 Agent 和隔离数据目录。 +- [x] **T2.5** 明确 `200 accepted` 只表示触发已接收,响应没有运行结果标识;验证用户可见的后台结果和失败记录观察路径。 +- [x] **T2.6** 核对重复调用与超时的真实行为。当前合同没有公开幂等键时,禁止建议发送方无条件自动重试;说明重复触发可能产生多次执行。 +- [x] **T2.7** 验证关闭/不存在 Agent、错误 secret、非法 body、未知事件、超大 body 和后台失败的处理;只补充现有测试未覆盖且影响公开承诺的回归用例。 +- [x] **T2.8** 交付中英文 Webhook 指南与端点参考,更新清单和首页/顶部入口。 + +验收:按公开步骤完成配置、发起一次触发并看到实际结果;成功响应、执行失败和请求失败能被区分。若现有结果观察不足,列出需要补齐的最小合同,例如稳定执行引用及受鉴权结果读取;独立代码任务合入并核验后再发布相应能力。仅作为异步触发发布时,页面应直接说明其有限结果能力。 + +### P3 扩展只读数据与对话运行流程 + +前置条件:固定基线后即可开始来源核查。只读端点在自身认证、归属和响应合同确认后独立编写与验证;对话执行在 HTTP 与 Socket.IO 合同确认后进入实现与验证。发布按各自条件推进,既不等待 P2,也不让只读参考等待完整 Socket.IO 流程。 + +- [x] **T3.1** 按真实场景选择 Agent 列表、Run 状态、历史和回执等只读端点;验证可信用户与资源归属。 +- [x] **T3.2** 给出 Socket.IO 客户端认证、连接、订阅、HTTP 提交和请求关联的最小完整 Node 示例,选择已有客户端库作为验证对象;核对 `/socket.io/`、`auth.token`、稳定 `clientInstanceId` 和 HTTP/Socket 的同一认证主体条件。 +- [x] **T3.3** 核对 `clientRequestId`、`originSocketId`、conversation/session/Run 的实际含义,明确哪个 ID 对应一次提交,哪个可跨多轮复用。 +- [x] **T3.4** 覆盖 `query:started`、follow-up 接纳、消息增量和终态等实际需要的事件;事件名与字段取自共享 Schema 和处理器。 +- [x] **T3.5** 说明 HTTP 响应和 Socket 事件无固定到达顺序,以及接纳、持久提交、运行和完成的区别。 +- [x] **T3.6** 验证同请求 ID 重试、请求体变化冲突、接纳结果未知、断线重连和结果对账;不要把未来事件回流写成离线事件重放。 +- [x] **T3.7** 核对取消、审批等待和错误恢复的当前支持范围;只使用已经验证的入口。 +- [x] **T3.8** 将通过验收的只读参考与完整对话示例分别交付中英文页面,更新端点清单;可以同批集成,任一就绪内容也可独立交付。 + +验收:只读参考分别完成可执行接入、权限/归属检查和响应验证,即可独立交付;对话流程则要求外部测试客户端能从授权连接开始,提交任务并关联结果,处理一次断线或接纳未知情况。不得把对话流程写成简单 POST 后同步返回最终答案,也不得将历史日志描述为可以重放全部副作用。 + +### P4 按需开放模型代理与工作流说明 + +前置条件:固定基线后即可分别核查模型代理与 Workflow。每项方法、地址、认证和参数合同确认后,编写页面、验证示例与必要代码补齐可按文件归属并行。发布时各自满足第 3.2 节条件,不依赖 P2/P3 或另一项 P4 能力完成。 + +- [x] **T4.1** 验证通过现有 `InspectModels`/`ComputeCredential` 等受支持工具取得 Compat Proxy 地址与 `desirecore_` 安全 token 的实际流程,判断该流程是否可作为长期外部接入合同。内部进程 capability 不作为外部凭据获取方法;管理端点不被误写成 token 签发端点。 +- [x] **T4.2** 核对安全 token 的 Provider 绑定、撤销、生命周期、模型列表范围和实际权限;不宣传尚未实现的预算/模型级限制。 +- [x] **T4.3** 分别验证 Messages、Chat Completions 和 Models;按实际 Provider/后端说明兼容子集、流中错误、用量与重试,不承诺完整供应商 API 覆盖。 +- [x] **T4.4** 保留用户可判断的供应商授权条件;代理接入不改变账户和订阅的使用资格。 +- [x] **T4.5** 核对 Workflow Service 的独立 origin、`X-Workflow-Service-Token`、当前端点和额外执行授权。旧 `POST /api/workflows` 已退役,不能进入新示例。 +- [x] **T4.6** 验证工作流 webhook 与 run 入口各自的主体、定义引用、执行授权和结果可见性;`executionGrantId` 等授权字段必须通过受支持流程取得。 +- [x] **T4.7** 对通过核验的能力分别创建中英文指南;缺少实际获取流程的能力继续记录在内部台账。 + +验收:本批发布的每项能力各有可执行接入闭环、对应凭据和实际协议范围,支持条件与现行治理规则一致。尚未发布的能力继续记录在内部台账。 + +### P5 提供 OpenAPI 与持续维护检查 + +前置条件:清单格式和某个候选公开端点的请求/响应合同已确认,即可生成该端点 spec 草稿。清单校验器在格式与 slug 约定固定后开始,不等待全量页面或 spec。下载发布与相应页面的公开条件一起验收,不必等页面已经上线后才编写 spec。 + +- [x] **T5.1** 从公开清单建立 Agent Service 的 OpenAPI 子集;保留原路径、字段、状态码、错误体和媒体类型。 +- [x] **T5.2** 尽量复用共享请求/响应 Schema;缺少响应 Schema 时先从处理器与验证结果确认合同,不凭模板补造响应壳。 +- [x] **T5.3** 描述静态 spec 的适用部署和权限限制;多个认证条件或额外治理不能被一个通用 Bearer 定义掩盖。 +- [x] **T5.4** Socket.IO 契约继续单列事件文档,避免伪造普通 HTTP/SSE 端点。Workflow 和 Compat Proxy 若提供 spec,使用独立文件与服务器定义。 +- [x] **T5.5** 校验 OpenAPI 文法、operationId、paths、示例与 Schema,确认与该 spec 声明的服务及端点子集一致;清单记录对应 spec,下载页说明尚未覆盖的公开能力。记录规范版本,保留客户端版本的独立意义。 +- [x] **T5.6** 发布下载页与 `static/openapi/` 文件,确认两种语言能访问同一份机器合同。 +- [x] **T5.7** 为公开页面/清单/spec 一致性加入适量自动检查,发现端点或版本差异时要求来源复核。 + +验收:下载文件只包含公开合同,声明的操作和示例通过检查。首批保持 Markdown/MDX 展示;只有端点数量和检索需求确实需要时,再评估 API 渲染组件或 SDK 生成。 + +## 7. 端点页面模板与写作要求 + +每个端点或单一能力页按以下顺序组织: + +1. 能做什么、适合哪个场景、在哪里使用。 +2. 支持的客户端/部署形态、所需配置、权限与授权条件。 +3. 获取服务地址和凭据的操作步骤。 +4. 实际 Method、Path、Headers、参数表、可解析请求示例。 +5. 实际响应与完成判定,异步接口的结果观察方式。 +6. 端点自己的错误字段、HTTP 状态、触发条件和调用方动作。 +7. 已支持的重试、幂等、取消、分页或恢复行为及限制。 +8. 完整调用示例、用户可见的排查方法、相关指南。 + +按 `AGENTS.md` 与 `README.md` 执行生效版本规范。已有明确版本保留真实编号;确需标注且无法确定的已可用功能使用 `v10.0.178+`。中文标题“生效版本”、英文标题“Effective version”,版本号格式 `vX.Y.Z+`。只约束相关功能,不能把默认版本用作未发行功能已经可用的证据。协议修订、配置版本和示例版本保留原含义。 + +代码示例只使用测试数据和占位凭据;变量的来源要讲清楚。不能包含真实 token、用户内容、机器上的绝对私有目录,或无法复制执行的 JSON 注释。公共页面不放开发分支、源码配置快照、PR 合入或作者验收告诫;这些内容放进维护台账。 + +同时核对现有集成概览中的审计承诺。只描述实际记录的内容与可见入口,避免把“所有请求响应完整记录”或“满足企业合规”作为未经验证的通用保证。 + +## 8. 验证与发布 + +### 8.1 实施过程验证 + +| 验证层 | 必须检查的内容 | +|---|---| +| 来源核对 | 方法/路径、注册条件、认证、治理、Schema、状态码与结果合同 | +| 示例验证 | 请求可解析并通过 Schema;公开步骤在支持形态中成立;结果判定准确 | +| 行为验证 | 成功、无效凭据、无效输入和该端点关键异步/权限失败;复用已有测试 | +| 语言与版本 | 中文/英文契约一致;已有版本保留;有效版本与协议编号不混用 | +| 站点构建 | 两种语言构建、broken links、稳定 slug、HTML 元信息和 sitemap | +| 页面体验 | 语言切换保持同页、sidebar、入口、代码复制、长路径/表格窄屏阅读 | + +首批不连接真实生产凭据、不执行有真实外部副作用的任务。运行验证使用固定版本、测试 Agent 和隔离数据根,记录支持的平台与部署形态;未覆盖形态不写成已验证支持。只针对具体未解决风险补充测试。 + +优先复用的现有验证入口(相对于 `desirecore`): + +| 范围 | 已有测试文件 | +|---|---| +| Webhook 权限与结果记录 | `packages/agent-service/src/__tests__/webhook-permissions.test.ts`、`packages/agent-service/src/routes/__tests__/webhook-state.test.ts`、`webhook-prompt-manifest.test.ts` | +| 实例与配对凭据 | `packages/agent-service/src/__tests__/instance-auth.test.ts`、`instance-auth-attached-client.test.ts`、`standalone-config.test.ts` | +| 对话来源与消息回流 | `packages/agent-service/src/__tests__/query-routes-follow-up-origin.test.ts`、`renderer-delivery.test.ts` | +| 代理安全 token | `packages/agent-service/src/__tests__/compat-proxy/server-safe-token.test.ts`、`packages/agent-service/src/compute/__tests__/safe-token.test.ts` | +| Workflow 授权与退役入口 | `lib/workflow-service/__tests__/access-auth.test.ts`、`definition-write-retirement.test.ts`、`run-admission.test.ts`、`semantic-run-grant.integration.test.ts`、`http-run-smoke.integration.test.ts` | + +表中同一单元格的简写文件名沿用前一个完整目录;执行前核对当前测试脚本及其依赖。既有 smoke 也需确认适配当前合同,测试通过不代替公开示例的发行版本验证。 + +建议执行命令: + +```bash +pnpm install --frozen-lockfile +pnpm typecheck +pnpm build +pnpm exec docusaurus serve --host 127.0.0.1 --port 3000 +``` + +新增清单校验器后执行 `pnpm api:check`。`pnpm build` 应构建中文和英文;现有配置 `onBrokenLinks: 'throw'` 必须保留。API 页面读取、语言切换与导航至少检查 1440、1280、1024 和 375 像素宽度;只针对溢出或阅读问题调整现有样式。 + +本地检查生成 HTML 的 canonical/hreflang,以及 `build/sitemap.xml`、`build/en/sitemap.xml` 的新增页面和暂缓页面范围。现有 `scripts/check-sitemap-links.mjs` 即使读本地 XML,也访问其中的生产 URL;部署前不能把它当本地预览验证。 + +### 8.2 Git 发布与自动部署 + +- [x] **T6.1** 审阅最终 diff、支持矩阵、脱敏验证记录和中英文页面。 +- [x] **T6.2** 按批次创建 `codex/` 分支与 PR;涉及其它仓库的前置修正分别发布并记录依赖。 +- [ ] **T6.3** 进入发布流程后,将已验证改动合并/推送到公开仓库 `main`,等待自动部署结果。按仓库规范无需手动执行 Vercel 部署命令。 +- [ ] **T6.4** 核对线上 `/api`、`/en/api`、已公开叶子页面、语言切换及 OpenAPI 下载(若本批包含)。 +- [ ] **T6.5** 部署成功后执行 `pnpm sitemap:check`,验证生产 URL、canonical、robots、跳转和软 404。 +- [ ] **T6.6** 检查新增内容的搜索可发现性;区分搜索爬取延迟与页面部署失败。 +- [ ] **T6.7** 回写本计划状态、公开端点清单与内部台账,记录最终代码/文档/发行基线。 + +发布后若发现错误的调用合同,先修正文档承诺与示例并重新走 Git 自动部署;必要时撤下有误的入口/下载项,保留合理迁移路径。仅回退公开文档提交不应修改客户端数据或权限配置。 + +## 9. 多 subAgent 并行实施 + +### 9.1 并发数量与调度规则 + +当前执行环境共有 4 个并发槽,采用 **1 个主 Agent + 最多 3 个活动 subAgent**。主 Agent 持续负责基线、跨模块决定、共享文件集成和发布;subAgent 完成独占的接口能力任务。下表中的工作角色可多于 3 个,但按可用槽滚动调度,不要求所有角色同时运行,也不假设能获得更多槽位。 + +P0—P5 表示工作范围,不能作为全局串行阶段。主 Agent 先完成 T0.1 并固定共同输入;之后按具体依赖派工,任一能力完成核查就启动其下游任务。某个能力有产品缺口时,该能力保留待解决任务,释放可用槽去做其它就绪工作。 + +默认优先保障接入基础和 Webhook 首批交付。接入基础的契约确认后,空闲槽立即推进只读数据/对话、代理、Workflow 或机器参考。只要存在就绪任务和空闲槽就继续调度,不等整批 subAgent 一起结束。 + +### 9.2 工作包与文件归属 + +每个工作包开始时明确唯一编辑者和完整文件清单;同一能力的中文、英文、示例和验证由同一个 subAgent 负责。需要另一 agent 校核翻译或协议时,校核者只返回修改意见,交由文件负责人应用。 + +| 工作包 | 对应任务 | 独占产物/编辑范围 | 输入依赖与交付方式 | +|---|---|---|---| +| 主 Agent:基线、发行和集成 | T0.1、T0.5、T0.7;各域 T0.6 的汇总;T1.6—T1.8;T6.1—T6.7 | `PLAN.md`、内部总台账、`api/endpoint-catalog.json`、`api/README.md`、API `index.md`/分类文件、sidebar、配置、所有入口页与全局翻译文件、根 README/AGENTS | 固定输入和文件归属;汇总领域记录,分批构建与发布 | +| subAgent 接入基础 | T0.2—T0.4 的 HTTP/认证/配对部分;T1.1—T1.5;T1.7 的模板建议 | `01-connection-and-auth.md`、`03-errors-and-troubleshooting.md` 及各自英文镜像;独立认证验证记录 | 基线确定后立即开始;其它领域提交自身错误事实,由本负责人整合到公共排查页 | +| subAgent Webhook | T0.2—T0.4 的 Webhook 部分;T0.6 的 Webhook 合同补充;T2.1—T2.8 | `02-agent-webhook.md` 与英文镜像;Webhook 配置/示例/测试;单独内部 Webhook 合同文件 | 与接入基础并行核查、编写;发布依赖对应认证/地址说明和实际配置/结果验证 | +| subAgent 只读数据与对话 | T0.2—T0.4 的 Agent/Run/Socket.IO 部分;T3.1—T3.8 | `04-conversations-and-runs.md` 与英文镜像;独立示例客户端和领域验证记录 | 核查可从固定基线开始;先使用已确认认证合同推进只读参考,再推进完整对话流程 | +| subAgent 模型代理 | T0.2—T0.4 的 Compat Proxy 部分;T4.1—T4.4、T4.7 的代理部分 | `05-model-proxy.md` 与英文镜像;代理示例/测试/验证记录 | 独立核查和验证地址、凭据、协议子集;向主 Agent 提交清单条目 | +| subAgent Workflow | T0.2—T0.4 的 Workflow 部分;T4.5—T4.7 的 Workflow 部分 | `06-workflow-integration.md` 与英文镜像;Workflow 示例/测试/验证记录 | 独立核查执行授权与结果合同;与代理和对话无阶段依赖 | +| subAgent 其它模块盘点 | T0.2—T0.4 中未分配的功能 API 文件 | 按文件分片的内部核查附件;尚未选择的其它端点不生成公开页面 | 主 Agent从完整文件清单分配互不重叠的模块;可在任何空闲槽推进,不阻塞已选择能力的首批交付 | +| subAgent OpenAPI | T5.1—T5.6 | 按服务分配的 `static/openapi/.json`、`07-openapi.md` 及英文镜像;spec 验证记录 | 每个端点合同确认即编写;接收领域负责人确认的 Schema/行为,按服务逐项完成 | +| subAgent 清单校验 | T5.7;第 5 节清单检查 | `scripts/check-api-docs.mjs`、独立校验输出 | 清单格式/slug 约定确定即可开始;`package.json` 命令由主 Agent统一接入 | + +表中领域的 T0.2—T0.4 是同一盘点任务的互斥分片,不重复全仓研究。主 Agent维护完整文件清单和分配状态,保证剩余功能模块也被覆盖。T0.5 由主 Agent汇总发行证据,各领域只提交自己端点的依据。T0.7 的最终公开决定和清单写入由主 Agent负责。T1.1 的 API 概览由主 Agent汇总,接入基础 subAgent 负责其余两页;其交付并停止编辑后,共享接入/错误页的后续补充可正式转交主 Agent。 + +需要补齐主仓代码时,按实际模块把源文件和对应测试纳入领域负责人清单。公共 `routes/index.ts`、认证中间件、共享 Schema 聚合出口、根依赖文件和 docs 子模块指针只能由主 Agent 或其指定的唯一负责人编辑;领域负责人提交所需改动说明,不自行重复修改。代码/合同改变后,主 Agent只重新调度受影响页面、示例和 spec 的校核。 + +### 9.3 滚动执行安排 + +| 调度时段 | 主 Agent | 槽 1 | 槽 2 | 槽 3 | 启动下游的条件 | +|---|---|---|---|---|---| +| 起步 | 固定 T0.1、分片范围、清单/slug 约定与共同源码基线 | 等待共同输入 | 等待共同输入 | 等待共同输入 | 输入固定后立即派工 | +| 首轮 | 核对发行证据,准备维护约定、独立 sidebar 和入口改动 | 接入基础核查与页面 | Webhook 核查、配置验证与页面 | 模型代理核查,提交确认部分后接 Workflow | 各能力分别提交事实;不等待全 P0 | +| 持续推进 | 接受领域事实并更新清单;准备首批集成 | 基础完成后转只读数据/对话 | Webhook 完成或出现外部阻塞后转 OpenAPI/校验 | 代理与 Workflow 按各自条件推进 | 谁先释放槽,就派当前最高优先级的就绪工作包 | +| 首批集成 | 冻结本批共享文件,完成统一构建与页面预览 | 校核首批协议和版本,或继续对话工作 | 校核首批示例/双语,或推进 spec | 继续代理/Workflow 或执行独立验证 | 审阅本批固定快照;后续写作不进入同一构建快照 | +| 后续交付 | 接收新能力、分批验证和 Git 发布 | 对话/只读域的剩余任务 | OpenAPI 或清单检查 | 代理/Workflow 的剩余任务 | 每项满足发布条件就进入下一可发布批次 | + +上述时段用于说明槽位复用,不设统一开始/结束屏障。主 Agent可以交换槽位任务;派工时只需保持文件唯一编辑者、明确依赖和共同输入。工作包已经完成的部分直接供下游使用,后续补充只通知受影响负责人。 + +### 9.4 具体依赖与可提前启动的工作 + +| 依赖节点 | 放行的工作 | 必须等待的工作 | +|---|---|---| +| 固定代码及子模块基线 | 所有领域的路由、权限、Schema 和版本依据核查 | 不同基线内容不能直接合并为同一合同 | +| 清单字段、operationId 和 slug 约定 | 清单校验器、站点结构、页面模板;各领域分别提交条目 | 全量清单写入、全局翻译生成由主 Agent执行 | +| 某能力的方法/地址/认证/请求响应合同确认 | 该能力中英文编写、示例验证、OpenAPI 草稿可并行 | 该能力上线仍需发行依据与完整验证 | +| 公共认证/配对合同确认 | 接入页、已筛选只读端点验证、对话客户端认证部分 | 查询回流部分仍等待该领域的 Socket.IO 身份与事件确认 | +| Webhook 的配置和权限入口确认 | Webhook 示例与错误场景验证 | 结果观察缺口影响该能力发布,不阻塞其它能力 | +| 某批页面/示例/spec 通过领域验收 | 主 Agent合并本批清单、入口与翻译,形成固定构建快照 | 双语构建、最终跨页校核和发布按固定快照顺序执行 | + +后续依赖按端点记录。例如一项只读端点的参考不依赖对话客户端实现;代理文档不依赖 Workflow;spec 可以与对应页面在同一个批次发布。`04-conversations-and-runs.md` 可先交付已经验证的只读章节,尚未成立的执行示例留在路由外草稿。读取源码、编辑独立文档和在隔离实例运行验证可以重叠执行。 + +### 9.5 交接格式与进度管理 + +主 Agent派工时向 subAgent 提供:目标与原任务编号、固定源码/子模块 SHA、独占文件清单、已经确认的认证/版本/slug 约定、所需输出和完成条件。subAgent 不重新 fetch 或切换公共 checkout;发现基线之后的相关改动时先报告,由主 Agent判断是否更新受影响输入。 + +每个领域交付一份简洁记录,包含实际方法/路径/注册条件、Schema 与认证/权限、发行依据、示例及验证结果、尚未成立的用户步骤、拟发布的中英文页面和清单条目。共同字段引用已有接入合同,不重复生成新的通用规则。成果可作为报告交接;需保存时按领域存放在内部台账的独立附件,主 Agent合并总表。 + +进度按“核查完成”“文档/示例完成”“验证完成”“本批已集成”“已部署”分别报告,避免把 subAgent 结束当作能力已发布。主 Agent负责更新本计划复选框;组合任务只有对应分片均完成时才勾选。存在待解决缺口的领域仍提交已确认事实,避免下游重复研究。 + +### 9.6 工作目录和验证隔离 + +- 持续写作与集成构建需要重叠时,优先采用主 Agent集成 checkout 加各领域独立 docs worktree;主 Agent只接收已验收文件或提交。仅有独立文档编辑、暂不构建时,也可在同一实施 checkout 按独占文件并行。共享文件保留唯一写入权;原负责人停止编辑并交付后才能转交归属。 +- 主仓代码补齐或运行实例验证按领域使用独立 checkout/工作目录、数据根、端口和测试进程;创建 worktree 时按仓库规范初始化并拉取最新子模块代码,记录基线后再派工。子模块与共同输入出现差异时由主 Agent统一处理。 +- 中文和英文由同一能力负责人完成;全局 `write-translations`、根目录依赖安装、锁文件更新、Git 切分与子模块指针由主 Agent串行执行。 +- 互不共享状态的领域验证可并行;先确认数据根重定向覆盖所有读写路径。Webhook 与对话需两套独立 Agent Service 实例;代理独占 token/算力配置和上游假服务;Workflow 独占定义 store、owner lock、运行历史与回调。使用同一 HOME、端口、进程级单例、环境配置或可写 fixture 的测试必须先隔离,无法隔离的测试串行执行。同一实例的 start/stop、token 轮换、状态重置与清理顺序执行。 +- 需要操作真实桌面 UI 时只指定一个活动操作负责人;其它 subAgent继续源码核对、文档或隔离 HTTP 验证。 +- 同一个 checkout 的 Docusaurus build、clear、dev/serve 和生成翻译操作由唯一负责人调度,避免竞争 `.docusaurus`、`build` 和生成的 `static/api/` 输出;只指定不同 build 输出目录不足以隔离这些写入。构建时固定本批输入,同 checkout 的 agent暂停修改会影响构建的文档/翻译/静态资源和配置;其它 subAgent 可在别的 checkout 或路由外草稿继续后续批次。生成目录不进入提交。 +- 独立源码审阅、双语校核和示例验证可同时进行;主 Agent收到结果后统一应用共享改动。每批只完成必要构建与验证,失败修正后重跑受影响检查。 + +### 9.7 必须串行的集成节点 + +必须由主 Agent协调的顺序是:共同输入固定 → 各领域确认所需合同 → 本批公开范围与共享文件合并 → 固定快照构建/预览 → Git 发布 → 等待自动部署 → 线上检查 → 回写状态。只有相关下游依赖这个顺序;其它领域的研究、编写和隔离验证继续并行。 + +最终集成前进行一次跨页核对:同一凭据、版本、端点和错误语义在基础页、能力页、清单和 spec 中一致;每个公开页面的中英文已齐备。解决接口解释冲突后再进入构建,不让多个 agent分别“修正”共享合同。 + +### 9.8 交付批次 + +| 批次 | 交付内容 | 完成条件 | +|---|---|---| +| 批次 1 | P0 台账、来源差异修正、首批范围与公开清单 | 候选有可核验出处,接入缺口具体化 | +| 批次 2 | P1 接入基础 + P2 Webhook 完整试点 | 优先一起交付;就绪的只读参考或 spec 可同时纳入,读者可以完成实际调用 | +| 批次 3 | P3 只读数据或 HTTP + Socket.IO 对话流程 | 各域分别满足权限与响应、或提交/结果/重试恢复验收,不互相等待 | +| 批次 4 | P4 模型代理或工作流指南 | 每项各自满足地址、凭据、权限和结果合同条件 | +| 批次 5 | P5 OpenAPI 与维护检查 | spec 与所声明的公开端点子集一致,下载页说明覆盖范围 | + +批次编号用于划分 review 与发布范围,不规定所有工作串行。首批通常由接入基础与 Webhook 构成;其它已经满足条件的能力或 spec 可提前纳入,尚未完成的领域继续并行。每批都完成第 8 节对应检查,不因提前集成而降低发布条件。 + +## 10. 待实施时解决的具体事项 + +| 事项 | 默认处理方向 | 解决阶段 | +|---|---|---| +| 首批支持哪个运行形态 | 优先核验部署所有者的 standalone 调用与专用 secret Webhook;其它形态按地址和凭据获取流程逐项确认 | P0 | +| Webhook 配置和结果观察是否能由目标用户操作 | 核查已有入口;有缺口时提出最小产品/代码补齐任务 | P0/P2 | +| Webhook 是否需要程序化结果与幂等 | 当前先按真实异步触发能力描述;明确业务场景需要时补稳定执行引用/查询和幂等合同 | P2 | +| 是否公开对话执行入口 | 以外部凭据与 HTTP + Socket.IO 完整流程为条件 | P3 | +| 代理凭据与地址是否有支持的获取方式 | 验证现有 ComputeCredential/受支持配置流程;缺口明确后再决定补齐方式 | P4 | +| 是否加入顶栏 | 首个完整调用流程就绪后加入,验证现有导航折叠与窄屏布局 | P1/P2 | +| 是否引入 OpenAPI 渲染组件或 SDK | 首批维持现有工具;按端点规模、兼容需求和实际使用者再评估 | P5 后 | + +这些事项是实施任务,当前计划可直接用于逐项核查。执行者应先完成能推进的任务,再针对确实依赖产品选择的缺口给出具体方案。 + +## 11. 计划修订记录 + +| 日期 | 修订 | 内容 | +|---|---|---| +| 2026-10-13 | 初版 | 固定初步代码基线,定义公开条件、站点落点、P0—P5 任务、分批交付和验证/自动部署流程 | +| 2026-10-13 | 并行实施修订 | 改为主 Agent + 3 subAgent 滚动调度,按能力确认合同后启动下游;明确独占文件、双语归属、工作目录隔离、共享集成节点和可重叠发布批次 | +| 2026-10-13 | 首批本地实施 | 完成双语接入基础与预配置 Webhook 参考、站点入口、维护清单与验证;记录自助配置和结果关联缺口,未执行发布 | + +实施中更新任务复选框与验证依据;范围、协议或发布策略变化时,在本表记录理由与影响,保持正文为当前有效计划。 + +## 12. 完整实施进度(2026-10-14) + +- 主 Agent:共享入口、24 个端点清单、79 份内部 API 文档盘点、旧设计/响应校准,源码最小接入缺口、统一验收与发布。 +- subAgent 对话/审阅:可运行 Node.js 示例、12 个隔离合同用例和真实路由夹具;Webhook 指南与并发权限快照交叉审阅。 +- subAgent 指南/运行:对话与 Run、模型代理、Workflow 双语指南,隔离代理/工作流链路验收。 +- subAgent 规范/验证:三个 OpenAPI 子集、真实 owner Webhook 运行与用户交付验证、规范检查。 + +新增代码复用 owner 治理、工具注册表、文件范围求交、Agent 原子写入及既有审批。Webhook 新配置入口只在握手声明支持时使用;无请求级结果 ID、通用幂等与自动最终回答交付的限制保留。代理地址取实际成功监听端口。Workflow 的 grant 运行入口不进入公共示例。 + +刷新发现 `origin/dev` 已推进到 `c09c3196ad3a5430aa4d4fc100769db0d6d0527b`,包含 query 可选应用准入与后台工具过滤。源码 PR 将在该基线重新集成,并只重验受影响合同;最终来源 SHA 与证据回写维护文件。 + +### 最终本地验收 + +接口代码固定 `d3bac348db5f15ce6a59469ffef8f495c84ced59`,最新rebase只合并docs指针,保留并行应用文档;接口代码与118项定向回归的代码树一致。内部文档固定 `10b6007b7e46477ee4df2e15563f09c1ab7bd1fe`,远程main可达。原80份固定快照另补并行新增的应用接口文件,81份API文档与855个字面量路由调用用于发现,公开24操作逐项验证,内部新应用参考保持内部分类。 + +发行线索核对 `v10.0.180` tag `5537110f3b286736f12d5babff66eabf48d59368`;存在旧query/Webhook/proxy/workflow路由不等于新管理能力已发行。新功能用实际握手能力条件,未反推首次发行号或追加默认版本。 + +三份规范0错误,51个Schema/34个media示例通过。双语构建、元信息与下载副本一致性通过,四种宽度六页无整页溢出,语言切换保持对话同页。代码复制仅确认按钮反馈,剪贴板内容未取得。 + +### PR 与 CI 验收 + +源码 [#4798](https://github.com/desirecore/desirecore/pull/4798) 与公开文档 [#72](https://github.com/desirecore/docs/pull/72) 已创建。Vercel预览成功、CLA通过;源码剩余主门禁继续验收。CodeQL指出XML实体串联替换可能重复解码,校验器改为单次正则匹配,保留嵌套编码一层;该修复不改变页面或接口合同。尚未合并上线。 + +### 上游门禁归因与发布依赖 + +本次新增能力的2处元信息期望遗漏已修复(4个单文件用例通过);另外16失败由固定CI基线的9文件16用例隔离实跑完整复现,证据见 `api/ci-base-attribution.json`。上游PR #4793已包含其对应修正,但新增application-object fixture暴露导航测试清单遗漏。独立PR #4801仅同步测试类型清单,并保持current pin、consumer错配及注入拒绝断言;11单文件用例和13受控类型边界通过。 + +发布依赖顺序为 #4801 → 上游 #4793 → API代码 #4798 → 公开文档 #72 → 自动部署和线上验收。代码与文档实施已完整,合并与上线仍须完整门禁成立;不把基线复现失败统计成成功验收。 + +### 上游衔接验收 + +#4801 已合并到 dev(`c1cdd01bb15e1b9db9ec8c64ae5a4cf88fbaa3bb`)。上游 #4793 最新 `33926b9d243b4f2bd6ea36b2e7634e347ec33d6b` 相比先前检查版本新增应用对象导航,但本轮24个公开端点及 query/Socket.IO/Run 合同无变化。 + +隔离工作树提前衔接该上游,得到本地快照 `66ee3deb567a7bbcbe1bde2f48f352a129c39ec0`,docs 采用已推送且保留双方变更的 `f620bc410063301cb5828f2ad63f9a10ce61f086`。限定11个API生产文件与已测 `74058cea9` 完全一致;单 worker 串行补验18项unit和1项真实Webhook管理integration全部通过,ADR/代码声明检查通过。该快照尚未推送,不替代当前PR的完整CI。 + +截至本次记录,#4793 的 Board Engine、资源测试和三平台native检查通过,static/unit仍运行;#72 的CodeQL、CLA与Vercel预览全部通过。部署后16个双语页面与8个下载地址的生产验收脚本已准备;尚未执行生产检查或宣称上线。 + +## 13. 发布衔接(2026-10-15) + +上游 #4793 的 static/unit/resource 与三平台检查全部通过,已合并为 `27b5709681f3ede8f5bce8eae02e925fd3f38dd3`。源码 #4798 最终 HEAD `3801f4d79cd4e578b8f3e006913a0c552e3763f3` 的 CI、Standalone、Board Engine、三平台 native 检查全部通过,已合并为 `e45f22902af2dc45b109cfa8f454795538aea2ed`;不重复合并已完成的 PR。 + +来源清单与三个规范固定到最终源码,内部 docs 指针为远程可达的 `9cc47fc9216db4a3803a982f00649f7af667c933`。33个端点来源/Schema文件仅认证 gate 导入方式调整,调用顺序和24端点合同不变。外围用量消息增加可选模型字段、回执按实际响应归集模型身份;现有指南与客户端兼容,不追加新字段保证或客户端版本约束。原定向运行结果仍按各自固定基线记录。 + +公开分支已衔接最新 main,保留三个发行版的并行更新日志。部署前生产/API路径及规范下载返回404、sitemap尚无API条目,作为上线前快照;上线验收须等公开PR合并与自动部署成功后执行。 diff --git a/README.md b/README.md index 1bb87cbf..0cff82a9 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,12 @@ Keep this convention and AGENTS.md consistent when updating either file. - Replace implementation-status caveats with an effective version, current support scope, or visible steps. Keep component names and technical details when they help with configuration, troubleshooting, permissions, or licenses. - Keep advice for documentation authors out of user-facing prose. Apply this approach in both languages while preserving actual limitations and authorization conditions. +### API documentation + +Public API guides live in `docs/07-api/`, with matching English files under `i18n/en/docusaurus-plugin-content-docs/current/07-api/`. Keep endpoint provenance and publication scope in [api/endpoint-catalog.json](./api/endpoint-catalog.json); follow [the API maintenance guide](./api/README.md) when updating contracts and examples. + +Run `pnpm api:check` to check the selected bilingual pages and JSON examples. After `pnpm build`, run `pnpm api:check -- --build` to check their local routes, language links, canonical URLs, and sitemap entries. The check does not certify client implementation or replace request and response validation. + ## License - Source code, theme customizations, and project configuration in this repository are licensed under the [MIT License](LICENSE). diff --git a/api/README.md b/api/README.md new file mode 100644 index 00000000..a653654f --- /dev/null +++ b/api/README.md @@ -0,0 +1,67 @@ +# 公开 API 文档维护 + +`docs/07-api/` 维护公开接入指南,英文镜像位于 `i18n/en/docusaurus-plugin-content-docs/current/07-api/`。本目录维护经过筛选的端点来源、公开范围与验证记录,不进入 Docusaurus 文档路由。 + +## 公开范围 + +8 组中英文页面覆盖接入认证、Agent Webhook 管理与触发、只读 Agent/Run/回执、HTTP + Socket.IO 对话、模型代理、工作流与 OpenAPI 下载。24 个 HTTP 操作按服务分成三个自包含 OpenAPI 3.1 文件;对话事件和恢复规则维护在指南与可运行 Node.js 示例中。 + +Webhook 所有者管理依赖握手 `capabilities.webhookManagement: true`,由可信入口选择实际工具并生成文件范围快照;旧服务仅支持已有事件。该支持条件没有被改写成未经核对的客户端发行号。现有 Write/Bash 与审批模型保留,不把配置文件宣称为不可修改的安全边界。 + +默认 Caddy 未代理 `/health`、Workflow 路径或本机 Compat Proxy。三种服务的地址、认证和权限分别维护。Workflow 只公开管理员预置定义的触发与观察,不伪造直接运行所需 grant 的公开签发流程。 + +独立示例包在 [examples/agent-client](./examples/agent-client/README.md),静态下载副本为 `static/api-examples/agent-client.mjs`;二者必须逐字一致。依赖和锁文件在示例目录维护,不改变站点依赖。 + +## 修改步骤 + +1. 固定核对的代码 SHA,核查实际 listener、路由、中间件、请求 Schema、响应和异步行为。 +2. 在 `endpoint-catalog.json` 更新相应操作;同一服务中的方法与路径、全局 `operationId` 不重复。来源使用仓库相对路径,不填写本机绝对路径。 +3. 修改对应中英文页面,保持字段、错误、版本约束和支持范围一致。一个能力由同一编辑负责人维护两种语言。 +4. 使用隔离测试数据验证实际请求和关键失败。区分 Schema 检查、mock query 的路由测试、真实 listener 测试和真实模型运行。 +5. 执行离线文档检查与双语构建,核对导航、代码复制、语言切换和窄屏阅读。 +6. 根据 Git 发布流程发布;自动部署成功后再检查生产 URL。当前 sitemap 检查器访问生产地址,不能替代部署前的本地检查。 + +```bash +pnpm api:check +pnpm typecheck +pnpm build +pnpm api:check -- --build +``` + +`api:check` 检查清单、两种语言的显式 slug、JSON 示例、版本提示框及 OpenAPI 范围、引用、鉴权和下载链接;`--build` 额外读取本地 HTML 和 sitemap。它不读取私有源仓库、不访问生产 URL,也不证明翻译语义或接口运行已验收。 + +## 清单字段 + +`pages` 列出本批公开页面的 `id`、源码 `file` 和稳定 `slug`。`endpoints` 使用以下字段: + +| 字段 | 用途 | +|---|---| +| `operationId`、`service`、`method`、`path`、`doc` | 操作身份、所属服务、实际路径和正文来源 | +| `deploymentModes`、`authentication`、`permissions` | 支持范围、认证和额外权限条件 | +| `resultDelivery` | 实际响应与结果交付方式 | +| `source`、`schemaRefs` | 固定来源 SHA、路由/认证文件和已有 Schema | +| `effectiveVersion`、`versionEvidence` | 确需版本约束时记录依据;无约束时为 `null`,不得推断首次可用版本 | +| `securitySchemes` | 该操作的实际认证方案,与规范逐项一致 | +| `verification` | 验证记录和未覆盖范围 | +| `spec` | 对应服务的公开 OpenAPI 文件;未收录项才为 `null` | + +遵循根 `AGENTS.md` 与 `README.md` 的生效版本和用户写作规范。已有明确版本不得改成默认值;协议修订、服务版本字段和客户端功能生效版本分别保留含义。 + +## 端点页面模板 + +一个能力的中英文页面按相同顺序组织,删除不适用的章节: + +1. 用途、目标读者与当前支持范围。 +2. 调用前准备:地址、凭据、配置和权限的实际获取方式。 +3. 方法、路径、请求头、参数表与合法 JSON 示例。 +4. 一次完整请求示例,以及实际响应和成功判定。 +5. 异步结果的观察方法与尚未提供的能力。 +6. 现行重试、去重、超时及恢复合同。 +7. 实际错误表:状态、字段/机器码、触发条件与调用方动作。 +8. 用户可操作的排查步骤和相关指南。 + +不要从模板补造响应壳、凭据签发、幂等或结果回调。来源、代码基线和验证记录写入本目录,用户正文只保留能帮助配置、调用和排查的技术细节。 + +## 当前验证记录 + +参见 [核查与验证](./verification.md)。后续更改认证、路由、配置入口或结果交付时,只重新核对受影响合同和相关示例,不将本批测试结果传播为其它功能的运行保证。 diff --git a/api/ci-base-attribution.json b/api/ci-base-attribution.json new file mode 100644 index 00000000..86377d52 --- /dev/null +++ b/api/ci-base-attribution.json @@ -0,0 +1,134 @@ +{ + "baseline": "5714a1899c0cc80d26463e596f1cadfcc1dd4533", + "platform": "darwin", + "architecture": "arm64", + "node": "v26.4.0", + "vitest": "5.0.3", + "scope": { + "files": 9, + "executed": 16, + "failures": 16, + "passed": 0, + "skippedCollectedCases": 2559, + "ciNameMatches": 16, + "excluded": "The two service-metadata cases are outside this baseline-attribution assignment." + }, + "submodules": { + "docs": { + "gitlink": "de4b857a48f350b2abba253d44657a195001fa01", + "checkedOut": "f620bc410063301cb5828f2ad63f9a10ce61f086", + "remoteDefault": "origin/main" + }, + "drawio": { + "gitlink": "88ee1f5ab4d40603a8f3aaa0d9d6dc3794c6b3f0", + "checkedOut": "88ee1f5ab4d40603a8f3aaa0d9d6dc3794c6b3f0", + "remoteDefault": "origin/dev" + } + }, + "dependencies": { + "workspacePackages": 12, + "workspaceLocation": "Each points to the baseline checkout packages directory.", + "thirdParty": "Reused installed dependencies entry-by-entry; published @desirecore/super-doc 0.2.16 is not a workspace." + }, + "failures": [ + { + "file": "app/stores/__tests__/chat-store-pending-message.test.ts", + "name": "chat.store 排队消息机制 多个会话的排队请求分组发送,不跨会话合并", + "signature": "AssertionError: expected \"vi.fn()\" to be called with arguments: [ 'A1\\nA2', ObjectContaining{…}, …(6) ]", + "seenInCI": true + }, + { + "file": "app/components/market/__tests__/MarketAgentDetail.test.tsx", + "name": "MarketAgentDetail 可信度与骨架 已安装 Agent 在 listing-only 阻断新获取时仍可卸载", + "signature": "AssertionError: expected true to be false // Object.is equality", + "seenInCI": true + }, + { + "file": "app/components/market/__tests__/MarketAgentDetail.test.tsx", + "name": "MarketAgentDetail 可信度与骨架 已安装 Agent 在 client-incompatible 阻断新获取时仍可卸载", + "signature": "AssertionError: expected true to be false // Object.is equality", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/__tests__/agent-git-conflicts.unit.test.ts", + "name": "commitAgentChange / auto-commit 阶段 Should call add when file exists with unstaged modifications / 文件被改但未暂存 → 调用 add", + "signature": "Error: ENOENT: no such file or directory, lstat '/tmp/test-facade-agents/test-agent-001/modified.txt'", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/__tests__/builtin-tool-definition-budget.test.ts", + "name": "内置工具定义体积预算 AppManage 的描述不超过上限", + "signature": "AssertionError: AppManage [posix / zh / 目录描述]: description=1050 rules=281 schema=678 total=2009: expected 1050 to be less than or equal to 600", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/__tests__/builtin-tool-schema-sync.test.ts", + "name": "内置工具 params 与 inputSchema 同步守卫 array/object 参数必须有结构(零豁免;有意开放用 additionalProperties: true 显式声明) AppManage.file_ref", + "signature": "AssertionError: AppManage.file_ref 的 inputSchema 缺少 properties/additionalProperties 结构。真自由形态请显式写 additionalProperties: true 并在工具文件注释说明有意开放。: expected false to be true // Object.is equality", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/__tests__/builtin-tool-schema-sync.test.ts", + "name": "内置工具 params 与 inputSchema 同步守卫 array/object 参数必须有结构(零豁免;有意开放用 additionalProperties: true 显式声明) AppManage.release_request", + "signature": "AssertionError: AppManage.release_request 的 inputSchema 缺少 properties/additionalProperties 结构。真自由形态请显式写 additionalProperties: true 并在工具文件注释说明有意开放。: expected false to be true // Object.is equality", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/__tests__/builtin-tool-schema-sync.test.ts", + "name": "内置工具 params 与 inputSchema 同步守卫 array/object 参数必须有结构(零豁免;有意开放用 additionalProperties: true 显式声明) AppManage.owner_request", + "signature": "AssertionError: AppManage.owner_request 的 inputSchema 缺少 properties/additionalProperties 结构。真自由形态请显式写 additionalProperties: true 并在工具文件注释说明有意开放。: expected false to be true // Object.is equality", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/__tests__/builtin-tool-schema-sync.test.ts", + "name": "内置工具 params 与 inputSchema 同步守卫 array/object 参数必须有结构(零豁免;有意开放用 additionalProperties: true 显式声明) AppManage.export_file_ref", + "signature": "AssertionError: AppManage.export_file_ref 的 inputSchema 缺少 properties/additionalProperties 结构。真自由形态请显式写 additionalProperties: true 并在工具文件注释说明有意开放。: expected false to be true // Object.is equality", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/__tests__/builtin-tools-all.test.ts", + "name": "Tool Registration / 工具注册 内置工具应全部注册 / All builtin tools should be registered", + "signature": "AssertionError: expected [ 'AppData', 'AppManage', …(119) ] to deeply equal [ 'AppManage', …(118) ]", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/__tests__/builtin-tools-all.test.ts", + "name": "Tool Registration / 工具注册 getAllBuiltinToolsAsRegistered 返回有效 RegisteredTool[] / getAllBuiltinToolsAsRegistered returns valid RegisteredTool[]", + "signature": "AssertionError: expected 121 to be 119 // Object.is equality", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/__tests__/builtin-tools-static-closure-budget.test.ts", + "name": "内置工具静态闭包预算 builtin-tools/terminal-control.ts 的静态闭包不超过 110 个模块", + "signature": "AssertionError: builtin-tools/terminal-control.ts 的模块初始化期静态闭包为 113,超过预算 110。", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/__tests__/builtin-tools-static-closure-budget.test.ts", + "name": "内置工具静态闭包预算 除已知遗留外,没有内置工具静态可达 socket-server", + "signature": "AssertionError: 这些内置工具的静态闭包里出现了 socket-server —— 一条新的间接回边成环了。", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/builtin-tools/__tests__/tool-text-translations-guard.test.ts", + "name": "内置工具文本的语言守卫 每个内置工具都有英文视图", + "signature": "AssertionError: expected [ 'AppData', 'ApplicationObject' ] to deeply equal []", + "seenInCI": true + }, + { + "file": "packages/agent-service/src/builtin-tools/__tests__/tool-text-translations-guard.test.ts", + "name": "内置工具文本的语言守卫 声明了英文视图的工具,英文视图里没有中文", + "signature": "AssertionError: expected [ …(21) ] to deeply equal []", + "seenInCI": true + }, + { + "file": "packages/schemas/src/agent-service/__tests__/embedded-service-process.test.ts", + "name": "Embedded Agent Service utility process frame schema 所有控制字段和 payload 字段都有 description", + "signature": "AssertionError: $.oneOf[8].properties.payload.oneOf[20].properties.params.properties.request.properties.resourceRef.oneOf[13].properties.pin 缺少 description: expected undefined to deeply equal Any", + "seenInCI": true + } + ], + "ciMergeRevision": "6bbd71a944419858e130e70429bba1931aaf0a89", + "ciFeatureRevision": "d3bac348db5f15ce6a59469ffef8f495c84ced59", + "interpretation": "All 16 selected failures reproduce at the fixed CI first-parent baseline. This record attributes failures; it is not a successful acceptance run. The additional two service-metadata expectations were repaired separately in the API feature branch." +} diff --git a/api/endpoint-catalog.json b/api/endpoint-catalog.json new file mode 100644 index 00000000..a2449db1 --- /dev/null +++ b/api/endpoint-catalog.json @@ -0,0 +1,877 @@ +{ + "schemaVersion": 1, + "reviewBaseline": { + "reviewedAt": "2026-10-15", + "clientRepository": "desirecore/desirecore", + "clientRevision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "internalDocsRevision": "9cc47fc9216db4a3803a982f00649f7af667c933", + "publicDocsBaseRevision": "34fb4a7cbc8bd2f453ee79536981d22c7448373b" + }, + "pages": [ + { + "id": "api/index", + "file": "docs/07-api/index.md", + "slug": "/api" + }, + { + "id": "api/connection-and-auth", + "file": "docs/07-api/01-connection-and-auth.md", + "slug": "/api/connection-and-auth" + }, + { + "id": "api/agent-webhook", + "file": "docs/07-api/02-agent-webhook.md", + "slug": "/api/agent-webhook" + }, + { + "id": "api/errors-and-troubleshooting", + "file": "docs/07-api/03-errors-and-troubleshooting.md", + "slug": "/api/errors-and-troubleshooting" + }, + { + "id": "api/conversations-and-runs", + "file": "docs/07-api/04-conversations-and-runs.md", + "slug": "/api/conversations-and-runs" + }, + { + "id": "api/model-proxy", + "file": "docs/07-api/05-model-proxy.md", + "slug": "/api/model-proxy" + }, + { + "id": "api/workflow-integration", + "file": "docs/07-api/06-workflow-integration.md", + "slug": "/api/workflow-integration" + }, + { + "id": "api/openapi", + "file": "docs/07-api/07-openapi.md", + "slug": "/api/openapi" + } + ], + "endpoints": [ + { + "operationId": "getAgentServiceClientHandshake", + "service": "agent-service", + "method": "GET", + "path": "/api/client-handshake", + "doc": "api/connection-and-auth", + "deploymentModes": [ + "standalone-owner", + "standalone-external" + ], + "authentication": "X-Agent-Service-Token from the deployment administrator", + "permissions": "Valid instance access credential; metadata is service-level and does not expose a user ID or token.", + "resultDelivery": "Synchronous JSON metadata. A 200 response verifies this authenticated request, not every optional capability.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/client-handshake-routes.ts", + "packages/agent-service/src/service-metadata.ts", + "packages/agent-service/src/auth/instance-auth.ts", + "packages/agent-service/src/readiness.ts", + "deploy/Caddyfile" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/client-handshake.ts:clientHandshakeResponseSchema" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Fixed-source auth and handshake unit tests plus a real loopback HTTPS readiness integration; no public-domain or Caddy-container run." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "triggerAgentWebhook", + "service": "agent-service", + "method": "POST", + "path": "/api/webhook/:agent_id", + "doc": "api/agent-webhook", + "deploymentModes": [ + "standalone-owner", + "standalone-external" + ], + "authentication": "Authorization: Bearer ; secret must be configured for external callers.", + "permissions": "Enabled event with an administrator-created tool/file permission snapshot. Owner management is available when handshake.capabilities.webhookManagement is true. Runtime intersects the stored ceiling with current Agent scope; authorized file or command tools retain existing approval semantics.", + "resultDelivery": "200 {accepted:true,agentId,event} acknowledges receipt, with no Run ID or delivery deduplication. Only explicit authorized SendUserMessage output is delivered to the user; final model answers are not automatically returned to the caller or added to chat.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/webhook-routes.ts", + "packages/agent-service/src/routes/webhook-state.ts", + "packages/agent-service/src/auth/instance-auth.ts", + "packages/agent-service/src/scheduler/background-delivery.ts", + "app/components/resources/agents/components/AgentFormEditor.tsx", + "deploy/Caddyfile" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/webhook.ts:webhookRequestSchema", + "packages/schemas/src/agent-service/webhook.ts:webhookEventConfigSchema" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Real production owner management, atomic AgentFS snapshot writes, local fake-provider runtime and SendUserMessage delivery, rotation, failure and disable/delete integration. No commercial-provider or packaged-client claim." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "WebhookSecret" + ] + }, + { + "operationId": "listAgents", + "service": "agent-service", + "method": "GET", + "path": "/api/agents", + "doc": "api/conversations-and-runs", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment administrator", + "permissions": "Owner access. Run list, metadata and live-state reads are instance-level; receipt reads additionally check trusted user ownership and return 404 for another owner.", + "resultDelivery": "Synchronous read; live state is observational, not a query completion signal.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/agent-routes.ts", + "packages/agent-service/src/agent/list-projection.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/agent-summary.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "listRuns", + "service": "agent-service", + "method": "GET", + "path": "/api/runs", + "doc": "api/conversations-and-runs", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment administrator", + "permissions": "Owner access. Run list, metadata and live-state reads are instance-level; receipt reads additionally check trusted user ownership and return 404 for another owner.", + "resultDelivery": "Synchronous read; live state is observational, not a query completion signal.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/runs-routes.ts", + "packages/agent-service/src/session/run-manager.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/run.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "getRun", + "service": "agent-service", + "method": "GET", + "path": "/api/runs/:id", + "doc": "api/conversations-and-runs", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment administrator", + "permissions": "Owner access. Run list, metadata and live-state reads are instance-level; receipt reads additionally check trusted user ownership and return 404 for another owner.", + "resultDelivery": "Synchronous read; live state is observational, not a query completion signal.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/runs-routes.ts", + "packages/agent-service/src/session/run-meta-read.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/run.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "getRunRuntimeState", + "service": "agent-service", + "method": "GET", + "path": "/api/runs/:id/runtime-state", + "doc": "api/conversations-and-runs", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment administrator", + "permissions": "Owner access. Run list, metadata and live-state reads are instance-level; receipt reads additionally check trusted user ownership and return 404 for another owner.", + "resultDelivery": "Synchronous read; live state is observational, not a query completion signal.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/runs-routes.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/run.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "listRunReceipts", + "service": "agent-service", + "method": "GET", + "path": "/api/runs/:id/receipt", + "doc": "api/conversations-and-runs", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment administrator", + "permissions": "Owner access. Run list, metadata and live-state reads are instance-level; receipt reads additionally check trusted user ownership and return 404 for another owner.", + "resultDelivery": "Synchronous read; live state is observational, not a query completion signal.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/runs-routes.ts", + "packages/agent-service/src/receipts/reader.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/operations-activity.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "startAgentQuery", + "service": "agent-service", + "method": "POST", + "path": "/api/query/start", + "doc": "api/conversations-and-runs", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment administrator", + "permissions": "Owner identity from instance authentication; durable request identifiers and digest preserve admission identity. Socket.IO uses the same instance credential and current originSocketId.", + "resultDelivery": "HTTP admission or lookup JSON; query completion and incremental messages arrive through correlated Socket.IO events.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/query-routes.ts", + "packages/agent-service/src/query/client-request-registry.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/query.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "followUpAgentQuery", + "service": "agent-service", + "method": "POST", + "path": "/api/query/follow-up", + "doc": "api/conversations-and-runs", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment administrator", + "permissions": "Owner identity from instance authentication; durable request identifiers and digest preserve admission identity. Socket.IO uses the same instance credential and current originSocketId.", + "resultDelivery": "HTTP admission or lookup JSON; query completion and incremental messages arrive through correlated Socket.IO events.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/query-routes.ts", + "packages/agent-service/src/query/client-request-registry.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/query.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "lookupQueryAdmissions", + "service": "agent-service", + "method": "POST", + "path": "/api/query/client-requests/lookup", + "doc": "api/conversations-and-runs", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment administrator", + "permissions": "Owner identity from instance authentication; durable request identifiers and digest preserve admission identity. Socket.IO uses the same instance credential and current originSocketId.", + "resultDelivery": "HTTP admission or lookup JSON; query completion and incremental messages arrive through correlated Socket.IO events.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/query-routes.ts", + "packages/agent-service/src/query/client-request-registry.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/client-request-admission.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Fixed-source request/response examples and focused HTTP + Socket.IO client contract; runtime fake in the public client fixture." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "getAgentWebhookConfiguration", + "service": "agent-service", + "method": "GET", + "path": "/api/agents/:id/webhooks", + "doc": "api/agent-webhook", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment owner", + "permissions": "Management requires governed owner access and webhookManagement capability; selected tool IDs and current file scope are resolved by the server. Schedule messages are explicit persisted user deliveries, not request-level execution status.", + "resultDelivery": "Synchronous configuration or explicit delivery JSON. First configuration and rotation disclose a secret once; GET does not return it.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/webhook-management-routes.ts", + "packages/agent-service/src/auth/governed-endpoint-auth.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/webhook-management.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "upsertAgentWebhookEvent", + "service": "agent-service", + "method": "PUT", + "path": "/api/agents/:id/webhooks/events/:event", + "doc": "api/agent-webhook", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment owner", + "permissions": "Management requires governed owner access and webhookManagement capability; selected tool IDs and current file scope are resolved by the server. Schedule messages are explicit persisted user deliveries, not request-level execution status.", + "resultDelivery": "Synchronous configuration or explicit delivery JSON. First configuration and rotation disclose a secret once; GET does not return it.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/webhook-management-routes.ts", + "packages/agent-service/src/auth/governed-endpoint-auth.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/webhook-management.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "setAgentWebhookEnabled", + "service": "agent-service", + "method": "PATCH", + "path": "/api/agents/:id/webhooks", + "doc": "api/agent-webhook", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment owner", + "permissions": "Management requires governed owner access and webhookManagement capability; selected tool IDs and current file scope are resolved by the server. Schedule messages are explicit persisted user deliveries, not request-level execution status.", + "resultDelivery": "Synchronous configuration or explicit delivery JSON. First configuration and rotation disclose a secret once; GET does not return it.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/webhook-management-routes.ts", + "packages/agent-service/src/auth/governed-endpoint-auth.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/webhook-management.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "deleteAgentWebhookEvent", + "service": "agent-service", + "method": "DELETE", + "path": "/api/agents/:id/webhooks/events/:event", + "doc": "api/agent-webhook", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment owner", + "permissions": "Management requires governed owner access and webhookManagement capability; selected tool IDs and current file scope are resolved by the server. Schedule messages are explicit persisted user deliveries, not request-level execution status.", + "resultDelivery": "Synchronous configuration or explicit delivery JSON. First configuration and rotation disclose a secret once; GET does not return it.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/webhook-management-routes.ts", + "packages/agent-service/src/auth/governed-endpoint-auth.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/webhook-management.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "rotateAgentWebhookSecret", + "service": "agent-service", + "method": "POST", + "path": "/api/agents/:id/webhooks/secret/rotate", + "doc": "api/agent-webhook", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment owner", + "permissions": "Management requires governed owner access and webhookManagement capability; selected tool IDs and current file scope are resolved by the server. Schedule messages are explicit persisted user deliveries, not request-level execution status.", + "resultDelivery": "Synchronous configuration or explicit delivery JSON. First configuration and rotation disclose a secret once; GET does not return it.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/webhook-management-routes.ts", + "packages/agent-service/src/auth/governed-endpoint-auth.ts" + ] + }, + "schemaRefs": [ + "packages/schemas/src/agent-service/webhook-management.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "listAgentScheduleMessages", + "service": "agent-service", + "method": "GET", + "path": "/api/agents/:agentId/schedule-messages", + "doc": "api/agent-webhook", + "deploymentModes": [ + "standalone-owner" + ], + "authentication": "X-Agent-Service-Token from the deployment owner", + "permissions": "Authenticated request identity; visibility is filtered by authorizedMessageIds using current delivery-resource authorization. This is persisted explicit delivery, not request-level Webhook execution status.", + "resultDelivery": "Synchronous {items,total,limit,offset}; default limit50, maximum200. Items may include other authorized background deliveries.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/agent-service/src/routes/schedule-routes.ts", + "packages/agent-service/src/auth/governed-endpoint-auth.ts" + ] + }, + "schemaRefs": [], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Production Webhook management integration with local fake model upstream and real delivery persistence; no commercial model or desktop package verification." + }, + "spec": "static/openapi/agent-service.json", + "securitySchemes": [ + "InstanceToken" + ] + }, + { + "operationId": "createProxyMessage", + "service": "compat-proxy", + "method": "POST", + "path": "/v1/messages", + "doc": "api/model-proxy", + "deploymentModes": [ + "local-loopback" + ], + "authentication": "Provider-bound safe token in Authorization: Bearer or x-api-key", + "permissions": "Local service access and valid non-revoked provider-bound safe token. Models listing is not an authorization list; no automatic token expiry or per-model quota promise.", + "resultDelivery": "Protocol-specific JSON or SSE stream; streaming failures are interpreted inside the stream.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/shared/src/compat-proxy/server.ts", + "packages/shared/src/compat-proxy/proxy-auth.ts", + "packages/agent-service/src/builtin-tools/compute-credential.ts" + ] + }, + "schemaRefs": [], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Focused local proxy contract tests; no commercial-provider acceptance." + }, + "spec": "static/openapi/compat-proxy.json", + "securitySchemes": [ + "ProxyBearer", + "ProxyApiKey" + ] + }, + { + "operationId": "createProxyChatCompletion", + "service": "compat-proxy", + "method": "POST", + "path": "/v1/chat/completions", + "doc": "api/model-proxy", + "deploymentModes": [ + "local-loopback" + ], + "authentication": "Provider-bound safe token in Authorization: Bearer or x-api-key", + "permissions": "Local service access and valid non-revoked provider-bound safe token. Models listing is not an authorization list; no automatic token expiry or per-model quota promise.", + "resultDelivery": "Protocol-specific JSON or SSE stream; streaming failures are interpreted inside the stream.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/shared/src/compat-proxy/server.ts", + "packages/shared/src/compat-proxy/proxy-auth.ts", + "packages/agent-service/src/builtin-tools/compute-credential.ts" + ] + }, + "schemaRefs": [], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Focused local proxy contract tests; no commercial-provider acceptance." + }, + "spec": "static/openapi/compat-proxy.json", + "securitySchemes": [ + "ProxyBearer", + "ProxyApiKey" + ] + }, + { + "operationId": "listProxyModels", + "service": "compat-proxy", + "method": "GET", + "path": "/v1/models", + "doc": "api/model-proxy", + "deploymentModes": [ + "local-loopback" + ], + "authentication": "Provider-bound safe token in Authorization: Bearer or x-api-key", + "permissions": "Local service access and valid non-revoked provider-bound safe token. Models listing is not an authorization list; no automatic token expiry or per-model quota promise.", + "resultDelivery": "Protocol-specific JSON or SSE stream; streaming failures are interpreted inside the stream.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "packages/shared/src/compat-proxy/server.ts", + "packages/shared/src/compat-proxy/proxy-auth.ts", + "packages/agent-service/src/builtin-tools/compute-credential.ts" + ] + }, + "schemaRefs": [], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Focused local proxy contract tests; no commercial-provider acceptance." + }, + "spec": "static/openapi/compat-proxy.json", + "securitySchemes": [ + "ProxyBearer", + "ProxyApiKey" + ] + }, + { + "operationId": "listWorkflows", + "service": "workflow-service", + "method": "GET", + "path": "/api/workflows", + "doc": "api/workflow-integration", + "deploymentModes": [ + "administrator-provided-workflow-service" + ], + "authentication": "X-Workflow-Service-Token for the independent Workflow Service", + "permissions": "Administrator-preconfigured definitions; service-token reads are service-level. TaskBoard-backed history additionally enforces current ACL. Public grant minting and unguarded /run are not included.", + "resultDelivery": "Synchronous metadata/history or trigger runId, followed by SSE workflow events; inspect terminal status even on workflow:complete.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "lib/workflow-service/server.ts", + "packages/shared/src/workflow-contracts/api.ts" + ] + }, + "schemaRefs": [ + "packages/shared/src/workflow-contracts/api.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Implementation-derived contract and focused workflow validation; no execution-grant acquisition flow." + }, + "spec": "static/openapi/workflow-service.json", + "securitySchemes": [ + "WorkflowToken" + ] + }, + { + "operationId": "triggerWorkflowWebhook", + "service": "workflow-service", + "method": "POST", + "path": "/api/workflow/webhook", + "doc": "api/workflow-integration", + "deploymentModes": [ + "administrator-provided-workflow-service" + ], + "authentication": "X-Workflow-Service-Token for the independent Workflow Service", + "permissions": "Administrator-preconfigured definitions; service-token reads are service-level. TaskBoard-backed history additionally enforces current ACL. Public grant minting and unguarded /run are not included.", + "resultDelivery": "Synchronous metadata/history or trigger runId, followed by SSE workflow events; inspect terminal status even on workflow:complete.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "lib/workflow-service/server.ts", + "packages/shared/src/workflow-contracts/api.ts" + ] + }, + "schemaRefs": [ + "packages/shared/src/workflow-contracts/api.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Implementation-derived contract and focused workflow validation; no execution-grant acquisition flow." + }, + "spec": "static/openapi/workflow-service.json", + "securitySchemes": [ + "WorkflowToken" + ] + }, + { + "operationId": "watchWorkflowRunEvents", + "service": "workflow-service", + "method": "GET", + "path": "/api/workflow/events", + "doc": "api/workflow-integration", + "deploymentModes": [ + "administrator-provided-workflow-service" + ], + "authentication": "X-Workflow-Service-Token for the independent Workflow Service", + "permissions": "Administrator-preconfigured definitions; service-token reads are service-level. TaskBoard-backed history additionally enforces current ACL. Public grant minting and unguarded /run are not included.", + "resultDelivery": "Synchronous metadata/history or trigger runId, followed by SSE workflow events; inspect terminal status even on workflow:complete.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "lib/workflow-service/server.ts", + "packages/shared/src/workflow-contracts/api.ts" + ] + }, + "schemaRefs": [ + "packages/shared/src/workflow-contracts/api.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Implementation-derived contract and focused workflow validation; no execution-grant acquisition flow." + }, + "spec": "static/openapi/workflow-service.json", + "securitySchemes": [ + "WorkflowToken" + ] + }, + { + "operationId": "listWorkflowRuns", + "service": "workflow-service", + "method": "GET", + "path": "/api/workflow/:id/runs", + "doc": "api/workflow-integration", + "deploymentModes": [ + "administrator-provided-workflow-service" + ], + "authentication": "X-Workflow-Service-Token for the independent Workflow Service", + "permissions": "Administrator-preconfigured definitions; service-token reads are service-level. TaskBoard-backed history additionally enforces current ACL. Public grant minting and unguarded /run are not included.", + "resultDelivery": "Synchronous metadata/history or trigger runId, followed by SSE workflow events; inspect terminal status even on workflow:complete.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "lib/workflow-service/server.ts", + "packages/shared/src/workflow-contracts/api.ts" + ] + }, + "schemaRefs": [ + "packages/shared/src/workflow-contracts/api.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Implementation-derived contract and focused workflow validation; no execution-grant acquisition flow." + }, + "spec": "static/openapi/workflow-service.json", + "securitySchemes": [ + "WorkflowToken" + ] + }, + { + "operationId": "getWorkflowRun", + "service": "workflow-service", + "method": "GET", + "path": "/api/workflow/:id/runs/:runId", + "doc": "api/workflow-integration", + "deploymentModes": [ + "administrator-provided-workflow-service" + ], + "authentication": "X-Workflow-Service-Token for the independent Workflow Service", + "permissions": "Administrator-preconfigured definitions; service-token reads are service-level. TaskBoard-backed history additionally enforces current ACL. Public grant minting and unguarded /run are not included.", + "resultDelivery": "Synchronous metadata/history or trigger runId, followed by SSE workflow events; inspect terminal status even on workflow:complete.", + "source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed", + "files": [ + "lib/workflow-service/server.ts", + "packages/shared/src/workflow-contracts/api.ts" + ] + }, + "schemaRefs": [ + "packages/shared/src/workflow-contracts/api.ts" + ], + "effectiveVersion": null, + "versionEvidence": null, + "verification": { + "record": "api/verification.md", + "scope": "Implementation-derived contract and focused workflow validation; no execution-grant acquisition flow." + }, + "spec": "static/openapi/workflow-service.json", + "securitySchemes": [ + "WorkflowToken" + ] + } + ] +} diff --git a/api/examples/agent-client/.gitignore b/api/examples/agent-client/.gitignore new file mode 100644 index 00000000..af5824de --- /dev/null +++ b/api/examples/agent-client/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +request.json +*.tmp diff --git a/api/examples/agent-client/README.md b/api/examples/agent-client/README.md new file mode 100644 index 00000000..7ae33dc7 --- /dev/null +++ b/api/examples/agent-client/README.md @@ -0,0 +1,68 @@ +# DesireCore Node conversation client + +This example calls an administrator's existing standalone Agent Service using the same owner credential for HTTP and Socket.IO. Obtain its HTTPS origin, existing instance token, and target Agent ID from the administrator. The Agent needs a working model configuration and permissions appropriate for your prompt. + +Use Node.js 22 or newer. Install dependencies **in this directory**, independently of the documentation website: + +```bash +npm install +export DESIRECORE_URL='https://agent.example.com' +export DESIRECORE_TOKEN='replace-with-your-existing-instance-token' +export DESIRECORE_AGENT_ID='replace-with-target-agent-id' + +node client.mjs --prompt 'Say hello in one sentence.' \ + --follow-up 'Translate your answer into Chinese.' \ + --state ./request.json +``` + +`DESIRECORE_URL` is an origin without `/api`, a page path, credentials, query, or fragment. HTTP is accepted only for loopback addresses used in local testing. Never disable certificate verification to make a remote connection work. The single-file download needs the same `socket.io-client` dependency beside it; install it in a dedicated directory with `npm install socket.io-client@4.8.4`. + +The client performs the authenticated read-only handshake, verifies the data-space instance ID, connects at `/socket.io/`, and supplies `auth.token` plus a stable `auth.clientInstanceId`. It submits HTTP queries with `X-Agent-Service-Token`, current `originSocketId`, and stable request/message IDs. The server automatically binds the authenticated connection's delivery room; the client does not emit a separate subscribe command. + +Text streams go to stdout. JSON progress records go to stderr. A `202` progress record means HTTP admission; it is not a final answer, durable commitment, or proof of a successful tool action. Only a matching `query:complete` with `terminal.status: "completed"` records completion of the current turn. Run `idle`, message `result`/`completed_idle`, and user message delivery are not treated as completion. An interrupted turn exits with status 1. Completion of a turn does not independently verify that every requested business operation succeeded. + +Each follow-up gets a new `clientRequestId` and message ID and reuses the current session. The client waits for the preceding turn to complete and checks that its admission belongs to the current server process. If the session is unavailable, it reports the failure; it does not silently create a new Run. Approval and question prompts need the instance's supported user interface; this CLI does not approve operations or answer questions automatically. + +## State and recovery + +`--state` must name a new file in an existing directory. The client saves request identity before sending, with file mode `0600`. The state contains prompts, queued follow-ups, and request/session/Run IDs, and never stores the access token. Keep it private and leave its frozen request bodies unchanged. Reusing an existing `--state` path is rejected. + +```bash +# Observe the recorded request without resending it or starting a new query. +node client.mjs --recover ./request.json + +# Reconnect to the saved request and continue waiting or safely retry its admission. +node client.mjs --resume ./request.json --timeout-ms 300000 + +# Explicitly request cancellation of the saved current request. +node client.mjs --cancel ./request.json --timeout-ms 30000 +``` + +`--resume`, `--recover`, and `--cancel` are separate modes. They use the instance origin and Agent ID in the state file; `DESIRECORE_TOKEN` must still contain a valid token. If `DESIRECORE_URL` is also set, it must match the saved origin. A different handshake `instanceId` prevents submission or recovery on the wrong instance. Only one client process should use a state file at a time: a new connection with the same client identity takes over delivery of future events. + +On an unknown admission, the client calls the read-only `POST /api/query/client-requests/lookup`. A `committed` request is not resent; `reserved` is awaited. `absent` may be resent using the exact same IDs, prompt, message, and digest within the replay and tombstone windows reported by the server; only `originSocketId` changes after reconnection. `unknown`, a previous-process commitment, or an expired replay window stops automatic resubmission. The `qrd1:` digest follows the shared UTF-8 FNV-1a contract and covers prompt, user message ID, media IDs, and file references; this text-only example uses empty media/file arrays. It is not a security hash and does not cover all query options. + +Socket reconnection receives future events; it does not replay missed events. Recovery reads admission state, the known Run's raw runtime state, and a bounded page of durable receipts. If the started event was missed but an exact session ID is known, the client searches up to 100 Agent-filtered Runs for a unique `session_id` match. It never guesses the latest Run. No match leaves the Run identity unknown. + +The receipt snapshot may contain other turns in the same Run. An idle/completed Run, a successful historical receipt, or a stored user message alone does not identify a missing terminal for this request. The example keeps that outcome unknown and does not send the next follow-up. Inspect the saved recovery facts and the instance's history before explicitly choosing another action. Recovery does not resume model execution or replay past tool effects. + +`Ctrl+C` stops the client and leaves the saved request available for recovery; the server may continue. `--cancel` sends Socket.IO `query:abort` using the saved Agent/conversation/request identity and session ID when known. Sending cancellation alone does not confirm cancellation; the client waits for the request's terminal event. A missing terminal still returns an unknown outcome. + +Exit codes: **0** = current turn explicitly completed; **1** = rejected, failed, or interrupted; **2** = unknown, read-only recovery, or a client/connection error. Timeouts are local waiting limits and do not automatically cancel server execution. `--http-timeout-ms` and `--poll-ms` control individual HTTP calls and admission polling. + +## Verification and maintenance + +Run `npm test` for the independent HTTP + Socket.IO contract fixture. It verifies request identity, start/follow-up, events arriving before HTTP, lost acknowledgements, reconnection, conservative recovery, body mismatch, interrupted turns, explicit cancellation, and prevention of false completion. The fixture is a fake instance, with no model credentials or real tool effects; passing it does not establish packaged-client or real-model acceptance. + +`test/source-route.fixture.ts` can be copied into a fixed-source DesireCore checkout as `packages/agent-service/src/__tests__/api-docs-node-client.integration.test.ts`. Set `DESIRECORE_API_EXAMPLE_PATH` to the absolute path of this directory's `client.mjs`, then run that single file in the repository's integration test project. It verifies this client against the actual query routes, HTTP/Socket authentication, request Schema and digest, durable admission registry and lookup, and renderer delivery room. The query runtime and follow-up execution callback remain fake. Use its isolated temporary data root; do not point it at an existing user profile. + +Source baseline: `desirecore/desirecore` commit `d3bac348db5f15ce6a59469ffef8f495c84ced59`. + +- HTTP submission: `packages/agent-service/src/routes/query-routes.ts`. +- Admission lookup: `packages/agent-service/src/routes/client-request-lookup-routes.ts`; shared Schema in `packages/schemas/src/agent-service/client-request-admission.ts`. +- Query request/digest: `packages/schemas/src/agent-service/query.ts`; `packages/shared/src/query/request-digest.ts`. +- Events: `packages/schemas/src/socket-events/{server-to-client,client-to-server}.ts`. +- Delivery identity: `packages/agent-service/src/handlers/renderer-delivery.ts` and `auth/delivery-principal.ts`. +- Run snapshots/receipts: `packages/agent-service/src/routes/runs-routes.ts`. + +After changing `client.mjs`, update `static/api-examples/agent-client.mjs` with identical bytes and revalidate the contract. The example is deliberately a small client, not a complete renderer, SDK, or durable execution orchestrator. diff --git a/api/examples/agent-client/client.mjs b/api/examples/agent-client/client.mjs new file mode 100644 index 00000000..514f8010 --- /dev/null +++ b/api/examples/agent-client/client.mjs @@ -0,0 +1,366 @@ +#!/usr/bin/env node +// DesireCore HTTP + Socket.IO example. No browser renderer or private IPC is required. +import { io } from 'socket.io-client'; +import { randomUUID } from 'node:crypto'; +import { readFileSync, writeFileSync, renameSync, existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { setTimeout as delay } from 'node:timers/promises'; + +export function queryDigest(prompt, userMessageId) { + // packages/shared/src/query/request-digest.ts: UTF-8 FNV-1a, not a security hash. + const canonical = JSON.stringify(['qrd1', prompt, userMessageId, [], []]); + let hash = 0xcbf29ce484222325n; + for (const byte of new TextEncoder().encode(canonical)) { + hash = ((hash ^ BigInt(byte)) * 0x100000001b3n) & 0xffffffffffffffffn; + } + return `qrd1:${hash.toString(16).padStart(16, '0')}`; +} + +const help = `Usage: + node client.mjs --prompt "Hello" [--follow-up "And ..."] --state ./request.json + node client.mjs --resume ./request.json + node client.mjs --recover ./request.json # read-only; does not resend + node client.mjs --cancel ./request.json # explicit cancellation request + +Environment: DESIRECORE_URL, DESIRECORE_TOKEN, DESIRECORE_AGENT_ID (new requests). +Options: --timeout-ms 300000, --http-timeout-ms 10000, --poll-ms 1000. +Use HTTPS remotely; HTTP is accepted only for localhost/127.0.0.1/::1. +Exit: 0 observed completed turn; 1 rejected/failed/interrupted; 2 unknown or recovery-only. +State files contain prompts and IDs, never the access token. Do not share them. +Ctrl+C exits this client without cancelling the server; use --cancel to request cancellation. +`; + +function argumentsFor(argv) { + const options = { followUps: [], timeoutMs: 300_000, httpTimeoutMs: 10_000, pollMs: 1_000 }; + const names = new Map([ + ['--prompt', 'prompt'], ['--state', 'state'], ['--resume', 'resume'], + ['--recover', 'recover'], ['--cancel', 'cancel'], ['--timeout-ms', 'timeoutMs'], + ['--http-timeout-ms', 'httpTimeoutMs'], ['--poll-ms', 'pollMs'], + ]); + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--help' || argv[i] === '-h') return { help: true }; + const option = argv[i]; + const value = argv[++i]; + if (!value || value.startsWith('--')) throw new Error(`Missing value for ${option}`); + if (option === '--follow-up') options.followUps.push(value); + else if (names.has(option)) options[names.get(option)] = value; + else throw new Error(`Unknown option ${option}`); + } + for (const key of ['timeoutMs', 'httpTimeoutMs', 'pollMs']) { + options[key] = Number(options[key]); + if (!Number.isSafeInteger(options[key]) || options[key] < 10) throw new Error(`Invalid ${key}`); + } + const modes = ['resume', 'recover', 'cancel'].filter(key => options[key]); + if (modes.length > 1 || (modes.length && (options.prompt || options.state || options.followUps.length))) { + throw new Error('Use either a new --prompt/--state or one of --resume/--recover/--cancel.'); + } + if (!modes.length && (!options.prompt?.trim() || !options.state)) throw new Error(help); + return options; +} + +function baseUrl(value) { + const url = new URL(value); + if (url.username || url.password || url.search || url.hash || url.pathname !== '/') { + throw new Error('DESIRECORE_URL must be an origin without credentials, a path, query, or fragment.'); + } + const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname); + if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) { + throw new Error('Use HTTPS for remote instances.'); + } + return url.origin; +} + +function newTurn(prompt, kind, sessionId, agentId, conversationId) { + const clientRequestId = randomUUID(); + const userMessageId = randomUUID(); + const requestDigest = queryDigest(prompt, userMessageId); + const body = kind === 'start' + ? { prompt, options: { agentId, conversationId }, clientRequestId, userMessageId, requestDigest } + : { sessionId, message: { id: userMessageId, role: 'user', content: prompt, timestamp: Date.now() }, clientRequestId, requestDigest }; + return { kind, body, userMessageId, firstDispatchedAt: null, firstDispatchedServerTime: null, attempts: 0 }; +} + +function readState(file) { + const state = JSON.parse(readFileSync(file, 'utf8')); + if (state.version !== 1 || !state.agentId || !state.instanceId || !Array.isArray(state.turns) + || !state.turns.length || !Array.isArray(state.pendingFollowUps) + || !/^[A-Za-z0-9._:-]{16,128}$/.test(state.clientInstanceId)) throw new Error('Invalid state file'); + state.baseUrl = baseUrl(state.baseUrl); + for (const turn of state.turns) { + const prompt = turn.kind === 'start' ? turn.body?.prompt : turn.body?.message?.content; + if (!['start', 'follow-up'].includes(turn.kind) || typeof prompt !== 'string' + || !turn.body?.clientRequestId || !turn.userMessageId + || turn.body.requestDigest !== queryDigest(prompt, turn.userMessageId)) { + throw new Error('Invalid or edited request state. Create a new state file for a different request.'); + } + } + return state; +} + +export async function runClient(options, env = process.env, output = {}) { + const log = output.log ?? (data => process.stderr.write(`${JSON.stringify(data)}\n`)); + const text = output.text ?? (value => process.stdout.write(value)); + const filename = resolve(options.resume || options.recover || options.cancel || options.state); + const existing = Boolean(options.resume || options.recover || options.cancel); + if (!existing && existsSync(filename)) throw new Error('State file already exists. Use --resume or a new --state path.'); + const token = env.DESIRECORE_TOKEN; + if (!token) throw new Error('Set DESIRECORE_TOKEN to the existing instance access token.'); + const origin = existing ? readState(filename).baseUrl : baseUrl(env.DESIRECORE_URL); + if (existing && env.DESIRECORE_URL && baseUrl(env.DESIRECORE_URL) !== origin) { + throw new Error('DESIRECORE_URL differs from the saved request; use the original instance.'); + } + const timeoutMs = options.timeoutMs ?? 300_000; + const httpTimeoutMs = options.httpTimeoutMs ?? 10_000; + const pollMs = options.pollMs ?? 1_000; + async function http(path, body) { + const response = await fetch(`${origin}${path}`, { + method: body === undefined ? 'GET' : 'POST', + redirect: 'error', signal: AbortSignal.timeout(httpTimeoutMs), + headers: { 'X-Agent-Service-Token': token, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + if (!response.headers.get('content-type')?.includes('application/json')) { + throw new Error(`HTTP ${response.status}: expected JSON from the instance API`); + } + return { status: response.status, data: await response.json(), retryAfter: response.headers.get('retry-after') }; + } + async function handshake() { + const deadline = Date.now() + httpTimeoutMs; + while (true) { + const result = await http('/api/client-handshake'); + if (result.status === 503 && result.data?.code === 'service_not_ready' && Date.now() < deadline) { + await delay(Math.max(1_000, Number(result.retryAfter || 1) * 1_000)); + continue; + } + if (result.status !== 200 || result.data.service !== 'desirecore-agent-service' + || !result.data.instanceId || result.data.apiProtocolVersion !== '1') { + throw new Error(`Handshake rejected: HTTP ${result.status} ${result.data.error || result.data.code || 'unsupported protocol'}`); + } + return result.data; + } + } + const metadata = await handshake(); + const clock = { local: Date.now(), server: Date.parse(metadata.serverTime) }; + if (!Number.isFinite(clock.server)) throw new Error('Handshake serverTime is invalid.'); + const state = existing ? readState(filename) : { + version: 1, baseUrl: origin, instanceId: metadata.instanceId, agentId: env.DESIRECORE_AGENT_ID, + conversationId: env.DESIRECORE_AGENT_ID, clientInstanceId: randomUUID(), + turns: [], pendingFollowUps: [...(options.followUps ?? [])], + }; + if (!state.agentId) throw new Error('Set DESIRECORE_AGENT_ID to the target Agent ID.'); + if (state.instanceId !== metadata.instanceId) throw new Error('Instance identity changed; refusing to send or recover requests on another instance.'); + if (!existing) state.turns.push(newTurn(options.prompt, 'start', undefined, state.agentId, state.conversationId)); + function save() { + const temporary = `${filename}.${process.pid}.tmp`; + writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 }); + renameSync(temporary, filename); + } + save(); + log({ event: 'handshake', instanceId: metadata.instanceId, serverVersion: metadata.serverVersion, state: filename }); + + async function lookup(turn) { + const result = await http('/api/query/client-requests/lookup', { + agentId: state.agentId, conversationId: state.conversationId, + requests: [{ clientRequestId: turn.body.clientRequestId, userMessageId: turn.userMessageId }], + }); + if (result.status !== 200) throw new Error(`Admission lookup: HTTP ${result.status} ${result.data.error || result.data.code || ''}`); + const verdict = result.data.results?.find(item => item.clientRequestId === turn.body.clientRequestId); + if (result.data.protocolVersion !== 1 || !['committed', 'reserved', 'absent', 'unknown'].includes(verdict?.status)) { + throw new Error('Unsupported admission lookup response'); + } + turn.admission = { ...verdict, serverInstanceId: result.data.serverInstanceId, observedAt: result.data.observedAt }; + if (verdict.sessionId) turn.sessionId = verdict.sessionId; + save(); + return { verdict, envelope: result.data }; + } + async function recovery(turn) { + const admission = await lookup(turn); + if (!turn.runId && turn.sessionId) { + const listing = await http(`/api/runs?agent_id=${encodeURIComponent(state.agentId)}&limit=100`); + const exact = listing.data.runs?.filter(run => run.agent_id === state.agentId && run.session_id === turn.sessionId); + if (listing.status === 200 && exact?.length === 1) turn.runId = exact[0].run_id; + } + const snapshot = { admission: admission.verdict, serverInstanceId: admission.envelope.serverInstanceId }; + if (turn.runId) { + const path = `/api/runs/${encodeURIComponent(turn.runId)}`; + const runtime = await http(`${path}/runtime-state`); + const history = await http(`${path}/receipt?offset=0&limit=10`); + snapshot.runtime = { httpStatus: runtime.status, ...runtime.data }; + snapshot.receipts = { httpStatus: history.status, ...history.data }; + } + turn.recovery = snapshot; + save(); + log({ event: 'recovery', clientRequestId: turn.body.clientRequestId, ...snapshot, + note: 'Run status and receipts are snapshots; they do not prove this request completed. Missing Socket events are not replayed.' }); + return admission; + } + let turn = state.turns.at(-1); + if (options.recover) { await recovery(turn); return 2; } + + const socket = io(origin, { + path: '/socket.io/', auth: { token, clientInstanceId: state.clientInstanceId }, + autoConnect: false, forceNew: true, reconnection: true, + }); + let disconnected = false; + let fatalSocketError; + socket.on('disconnect', reason => { disconnected = true; log({ event: 'disconnected', reason }); }); + socket.on('connect_error', error => { + log({ event: 'connect_error', message: error.message }); + if (['unauthorized', 'instance_token_uninitialized'].includes(error.message)) fatalSocketError = error; + }); + socket.on('connect', () => log({ event: 'connected', socketId: socket.id, clientInstanceId: state.clientInstanceId })); + function matching(data) { return state.turns.find(item => item.body.clientRequestId === data?.clientRequestId); } + function bind(data, item) { + if (data.sessionId) item.sessionId = data.sessionId; + if (data.runId || data.terminal?.runId) item.runId = data.runId || data.terminal.runId; + } + for (const name of ['query:started', 'query:followUpAccepted']) socket.on(name, data => { + const item = matching(data); + if (!item) return; + bind(data, item); item.acceptedEvent = name; save(); + log({ event: name, clientRequestId: data.clientRequestId, sessionId: item.sessionId, runId: item.runId }); + }); + socket.on('query:message', data => { + const item = matching(data); + if (!item || item !== turn || item.outcome) return; + bind(data, item); + const message = data.message; + const delta = message?.type === 'stream_event' ? message.event?.delta : undefined; + if (message?.event?.type === 'content_block_delta' && delta?.type === 'text_delta' && typeof delta.text === 'string') { + item.sawTextDelta = true; text(delta.text); + } else if (!item.sawTextDelta && message?.type === 'text' && typeof message.text === 'string') text(message.text); + else if (!item.sawTextDelta && message?.type === 'assistant') { + for (const block of message.message?.content ?? []) if (block.type === 'text' && typeof block.text === 'string') text(block.text); + } + // result/completed_idle/user:message_delivery and approvals are not terminal events. + }); + socket.on('query:complete', data => { + const item = matching(data); + if (!item || item.outcome) return; + bind(data, item); + item.outcome = { status: data.terminal?.status === 'completed' ? 'completed' + : data.terminal?.status === 'interrupted' ? 'interrupted' : 'unknown', + source: 'query:complete', reason: data.terminal?.reason, partial: data.terminal?.partial }; + save(); text('\n'); log({ event: 'turn_terminal', clientRequestId: data.clientRequestId, ...item.outcome }); + }); + socket.on('query:error', data => { + const item = matching(data); + if (!item || item.outcome) return; + bind(data, item); item.outcome = { status: 'failed', source: 'query:error', code: data.code, error: data.error }; + save(); log({ event: 'turn_terminal', clientRequestId: data.clientRequestId, ...item.outcome }); + }); + socket.on('activity:run_status', data => { + if (turn.runId === data.runId) log({ event: 'run_status', runId: data.runId, status: data.status, + note: 'Run state does not identify completion of this turn.' }); + }); + async function connected(deadline) { + while (!socket.connected) { + if (fatalSocketError) throw fatalSocketError; + if (Date.now() >= deadline) throw new Error('Socket connection timed out; saved request has not been automatically cancelled.'); + await delay(Math.min(pollMs, 100)); + } + } + async function dispatch(item) { + if (item.firstDispatchedAt === null) { + item.firstDispatchedAt = Date.now(); + item.firstDispatchedServerTime = clock.server + item.firstDispatchedAt - clock.local; + } + item.attempts++; save(); // Write request identity before performing the POST. + let response; + try { response = await http(`/api/query/${item.kind}`, { ...item.body, originSocketId: socket.id }); } + catch (error) { + log({ event: 'admission_unknown', clientRequestId: item.body.clientRequestId, message: error.message }); + return; + } + if (response.status === 202 && response.data.ok === true + && response.data.clientRequestId === item.body.clientRequestId) { + if (response.data.sessionId) item.sessionId = response.data.sessionId; + item.httpAccepted = true; save(); + log({ event: 'http_admission', status: 202, duplicate: response.data.duplicate === true, + clientRequestId: item.body.clientRequestId, note: 'Admission, not completion or durable commitment.' }); + } else if (response.status === 202) throw new Error('Admission response did not echo the request identity.'); + else if ([400, 401, 403, 404, 409, 413, 415].includes(response.status) && !response.data.retryable) { + item.outcome ??= { status: 'rejected', source: 'http', httpStatus: response.status, + code: response.data.code, error: response.data.error }; + save(); log({ event: 'turn_rejected', clientRequestId: item.body.clientRequestId, ...item.outcome }); + } else log({ event: 'admission_pending', status: response.status, code: response.data.code, + busy: response.data.busy === true, clientRequestId: item.body.clientRequestId }); + } + function replayAllowed(item, envelope) { + const age = Date.parse(envelope.observedAt) - item.firstDispatchedServerTime; + const window = Math.min(envelope.autoReplayRetentionMs, envelope.tombstoneRetentionMs); + return Number.isFinite(age) && age >= 0 && window > 0 && age < window; + } + socket.connect(); + try { + while (true) { + const deadline = Date.now() + timeoutMs; + await connected(deadline); + if (options.cancel) { + if (turn.outcome) { log({ event: 'already_terminal', outcome: turn.outcome }); return turn.outcome.status === 'completed' ? 0 : 1; } + socket.emit('query:abort', { ...(turn.sessionId ? { sessionId: turn.sessionId } : {}), + agentId: state.agentId, conversationId: state.conversationId, clientRequestId: turn.body.clientRequestId }); + log({ event: 'cancel_requested', clientRequestId: turn.body.clientRequestId }); + } else if (!turn.outcome && turn.firstDispatchedAt === null) await dispatch(turn); + + while (!turn.outcome && Date.now() < deadline) { + await delay(pollMs); + if (turn.outcome) break; + if (fatalSocketError) throw fatalSocketError; + if (!socket.connected) continue; + try { + const observation = disconnected ? await recovery(turn) : await lookup(turn); + disconnected = false; + if (turn.outcome) break; + const { verdict, envelope } = observation; + if (verdict.status === 'unknown') { + log({ event: 'outcome_unknown', clientRequestId: turn.body.clientRequestId, note: 'No automatic resend.' }); + return 2; + } + if (verdict.status === 'committed' && verdict.committedServerInstance === 'previous') { + await recovery(turn); return 2; + } + if (verdict.status === 'absent' && !options.cancel) { + if (!replayAllowed(turn, envelope)) { + log({ event: 'replay_window_expired', clientRequestId: turn.body.clientRequestId }); return 2; + } + await dispatch(turn); // Preserve IDs, prompt, digest, and message; replace only originSocketId. + } + // committed/reserved wait for this turn's events. Neither proves business completion. + } catch (error) { log({ event: 'reconciliation_unavailable', message: error.message }); } + } + if (!turn.outcome) { + try { await recovery(turn); } catch (error) { log({ event: 'reconciliation_unavailable', message: error.message }); } + log({ event: 'outcome_unknown', clientRequestId: turn.body.clientRequestId, note: 'Timed out locally; server may still run. Use --recover or --resume.' }); + return 2; + } + if (turn.outcome.status !== 'completed') return turn.outcome.status === 'unknown' ? 2 : 1; + if (!state.pendingFollowUps.length || options.cancel) return 0; + // Follow-ups reuse a live session; never silently create a successor Run after a restart. + const observed = await lookup(turn); + if (!turn.sessionId || observed.verdict.status !== 'committed' + || observed.verdict.committedServerInstance !== 'current') { + await recovery(turn); log({ event: 'follow_up_unavailable', note: 'A live current-process session was not confirmed.' }); return 2; + } + const next = state.pendingFollowUps.shift(); + turn = newTurn(next, 'follow-up', turn.sessionId, state.agentId, state.conversationId); + state.turns.push(turn); save(); + } + } finally { socket.disconnect(); save(); } +} + +export async function main(argv = process.argv.slice(2)) { + try { + const options = argumentsFor(argv); + if (options.help) { process.stdout.write(help); return 0; } + return await runClient(options); + } catch (error) { + process.stderr.write(`${JSON.stringify({ event: 'client_error', message: error.message })}\n`); + return 2; + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + process.exitCode = await main(); +} diff --git a/api/examples/agent-client/package-lock.json b/api/examples/agent-client/package-lock.json new file mode 100644 index 00000000..a0cee122 --- /dev/null +++ b/api/examples/agent-client/package-lock.json @@ -0,0 +1,311 @@ +{ + "name": "desirecore-agent-client-example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "desirecore-agent-client-example", + "version": "1.0.0", + "dependencies": { + "socket.io-client": "4.8.4" + }, + "devDependencies": { + "socket.io": "4.8.4" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmmirror.com/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmmirror.com/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io": { + "version": "6.6.11", + "resolved": "https://registry.npmmirror.com/engine.io/-/engine.io-6.6.11.tgz", + "integrity": "sha512-z34mGpLoY/82ZbkX1LqGMIaf3wjVMPL6gVC681QDqnmzJ26synI8qAE32P+a7UazmpJZ8UGrFIeAavgRfXzYrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.7", + "resolved": "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.6.7.tgz", + "integrity": "sha512-GyAvmPA7gnRCpDQbYACAy79Lg4ewevrmaPzySZz0OVvgyZc3gEfLLqPTYk8wVBScjOEpM9ZO4KryyFpVItjGEA==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.4", + "resolved": "https://registry.npmmirror.com/socket.io/-/socket.io-4.8.4.tgz", + "integrity": "sha512-9ZV9zg8vN8hO3Y25DhR7nUkJ1VDnBhq5O/JrXhFT9s6+F0gqytSASTHVfCFc2mJXaTfARroErplXlzFWwETZqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.8", + "resolved": "https://registry.npmmirror.com/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.4", + "resolved": "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.8.4.tgz", + "integrity": "sha512-7wjKAvVXYOJsbmUgkWbnhPD4bFpkqrUR0++XOjpGFmqDqsqV1CI93VTraeExLxpjxEqrWgFoSRZu1ihjLWfkAA==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + } + } +} diff --git a/api/examples/agent-client/package.json b/api/examples/agent-client/package.json new file mode 100644 index 00000000..b280aa54 --- /dev/null +++ b/api/examples/agent-client/package.json @@ -0,0 +1,11 @@ +{ + "name": "desirecore-agent-client-example", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "HTTP and Socket.IO client example for an administrator's DesireCore instance", + "engines": { "node": ">=22" }, + "scripts": { "start": "node client.mjs", "test": "node --test test/*.test.mjs" }, + "dependencies": { "socket.io-client": "4.8.4" }, + "devDependencies": { "socket.io": "4.8.4" } +} diff --git a/api/examples/agent-client/test/client.test.mjs b/api/examples/agent-client/test/client.test.mjs new file mode 100644 index 00000000..1c61185f --- /dev/null +++ b/api/examples/agent-client/test/client.test.mjs @@ -0,0 +1,203 @@ +// The HTTP/Socket.IO fixture models the public contract. It is not a DesireCore runtime. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { createServer } from 'node:http'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { Server } from 'socket.io'; +import { runClient, queryDigest } from '../client.mjs'; + +async function instance(scenario = 'normal') { + const requests = [], observations = [], cancellations = [], records = new Map(), timers = new Set(); + const later = (ms, callback) => { const timer = setTimeout(() => { timers.delete(timer); callback(); }, ms); timers.add(timer); }; + function json(response, status, body) { response.writeHead(status, { 'Content-Type': 'application/json' }); response.end(JSON.stringify(body)); } + const server = createServer(async (request, response) => { + if (request.headers['x-agent-service-token'] !== 'owner-test-token') return json(response, 401, { error: 'unauthorized' }); + const url = new URL(request.url, 'http://localhost'); + if (url.pathname === '/api/client-handshake') return json(response, 200, { + service: 'desirecore-agent-service', apiProtocolVersion: '1', instanceId: 'instance-test', + serverVersion: '10.0.180', serverTime: new Date().toISOString(), + }); + if (url.pathname === '/api/runs') return json(response, 200, { runs: [...records.values()].map(record => record.run), count: records.size }); + if (url.pathname.endsWith('/runtime-state')) { + observations.push('runtime-state'); + const record = [...records.values()].find(record => url.pathname.includes(record.run.run_id)); + return json(response, record ? 200 : 404, record ? record.run : { error: 'Run not found' }); + } + if (url.pathname.endsWith('/receipt')) { + observations.push('receipt'); + return json(response, 200, { run_id: 'run-test', receipts: [{ status: 'success', response: 'stored answer' }], total: 1, offset: 0, limit: 10 }); + } + let body; + try { let bytes = ''; for await (const chunk of request) bytes += chunk; body = JSON.parse(bytes); } + catch { return json(response, 400, { error: 'invalid JSON' }); } + if (url.pathname === '/api/query/client-requests/lookup') { + observations.push('lookup'); + return json(response, 200, { + protocolVersion: 1, serverInstanceId: 'server-generation-1', observedAt: new Date().toISOString(), + autoReplayRetentionMs: scenario === 'expired' ? 0 : 86400000, tombstoneRetentionMs: 604800000, + results: body.requests.map(item => { + const record = records.get(item.clientRequestId); + return scenario === 'unknown' ? { clientRequestId: item.clientRequestId, status: 'unknown' } + : record ? { clientRequestId: item.clientRequestId, status: 'committed', sessionId: record.run.session_id, + committedServerInstance: 'current', executionWitness: 'user-message' } + : { clientRequestId: item.clientRequestId, status: 'absent' }; + }), + }); + } + if (!['/api/query/start', '/api/query/follow-up'].includes(url.pathname)) return json(response, 404, { error: 'Not found' }); + requests.push({ path: url.pathname, body }); + const socket = sockets.sockets.sockets.get(body.originSocketId); + assert.ok(socket?.connected, 'HTTP origin must be a connected Socket.IO ID'); + assert.equal(socket.handshake.auth.token, request.headers['x-agent-service-token']); + const prompt = body.prompt ?? body.message.content; + const messageId = body.userMessageId ?? body.message.id; + assert.equal(body.requestDigest, queryDigest(prompt, messageId)); + if (scenario === 'conflict') return json(response, 409, { ok: false, code: 'CLIENT_REQUEST_BODY_MISMATCH', retryable: false }); + if (['lost-before-admission', 'expired', 'unknown'].includes(scenario) && requests.length === 1) { + request.socket.destroy(); + if (scenario === 'lost-before-admission') socket.conn.close(); + return; + } + const previous = records.get(body.clientRequestId); + if (previous) return json(response, 202, { ok: true, duplicate: true, clientRequestId: body.clientRequestId, sessionId: previous.run.session_id }); + const run = { run_id: 'run-test', agent_id: 'agent-test', session_id: body.sessionId ?? 'session-test', status: 'running' }; + records.set(body.clientRequestId, { run }); + const room = `renderer:${socket.handshake.auth.clientInstanceId}`; + const data = { sessionId: run.session_id, runId: run.run_id, agentId: run.agent_id, clientRequestId: body.clientRequestId }; + const emit = (name, payload) => sockets.to(room).emit(name, payload); + later(5, () => emit(url.pathname.endsWith('/start') ? 'query:started' : 'query:followUpAccepted', data)); + // An unrelated request in the same session must not steal this turn's outcome. + later(8, () => emit('query:complete', { ...data, clientRequestId: 'another-request', terminal: { status: 'completed', runId: run.run_id } })); + later(10, () => emit('query:message', { ...data, message: { type: 'stream_event', event: { + type: 'content_block_delta', delta: { type: 'text_delta', text: `answer:${prompt}` }, + } } })); + if (scenario === 'missed-terminal') later(15, () => socket.conn.close()); + later(25, () => { + run.status = 'idle'; + emit('query:message', { ...data, message: { type: 'result', status: 'completed_idle', text: 'not a terminal' } }); + emit('query:message', { ...data, message: { type: 'user:message_delivery', message: 'delivery is not completion' } }); + emit('activity:run_status', { runId: run.run_id, status: 'idle' }); + if (scenario !== 'false-terminal-only') emit('query:complete', { + ...data, terminal: { status: scenario === 'interrupted' ? 'interrupted' : 'completed', runId: run.run_id }, + }); + }); + if (scenario === 'lost-after-admission') request.socket.destroy(); + else if (scenario === 'events-before-http') later(60, () => json(response, 202, { ok: true, clientRequestId: body.clientRequestId, ...(body.sessionId ? { sessionId: body.sessionId } : {}) })); + else json(response, 202, { ok: true, clientRequestId: body.clientRequestId, ...(body.sessionId ? { sessionId: body.sessionId } : {}) }); + }); + const sockets = new Server(server, { path: '/socket.io/' }); + sockets.use((socket, next) => socket.handshake.auth.token === 'owner-test-token' ? next() : next(new Error('unauthorized'))); + sockets.on('connection', socket => { + socket.join(`renderer:${socket.handshake.auth.clientInstanceId}`); + socket.on('query:abort', data => { + cancellations.push(data); + socket.emit('query:complete', { sessionId: data.sessionId, clientRequestId: data.clientRequestId, + terminal: { status: 'interrupted', reason: 'user_abort', runId: 'run-test' } }); + }); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + return { requests, observations, cancellations, origin: `http://127.0.0.1:${server.address().port}`, + close: async () => { for (const timer of timers) clearTimeout(timer); await new Promise(resolve => sockets.close(resolve)); } }; +} + +async function execute(t, scenario, extra = {}) { + const service = await instance(scenario); + const directory = mkdtempSync(join(tmpdir(), 'desirecore-cli-test-')); + t.after(async () => { await service.close(); rmSync(directory, { recursive: true, force: true }); }); + const filename = join(directory, 'request.json'); + const logs = [], chunks = []; + const env = { DESIRECORE_URL: service.origin, DESIRECORE_TOKEN: 'owner-test-token', DESIRECORE_AGENT_ID: 'agent-test' }; + const options = { state: filename, prompt: 'hello', followUps: [], timeoutMs: 300, httpTimeoutMs: 200, pollMs: 20, ...extra }; + const code = await runClient(options, env, { log: data => logs.push(data), text: data => chunks.push(data) }); + return { service, filename, env, code, logs, chunks, state: JSON.parse(readFileSync(filename, 'utf8')) }; +} + +test('start and follow-up stream separately; identities persist and HTTP/Socket credentials match', async t => { + const result = await execute(t, 'normal', { followUps: ['again'] }); + assert.equal(result.code, 0); + assert.equal(result.service.requests.length, 2); + assert.equal(result.service.requests[1].body.sessionId, 'session-test'); + assert.notEqual(result.service.requests[0].body.clientRequestId, result.service.requests[1].body.clientRequestId); + assert.equal(result.chunks.join(''), 'answer:hello\nanswer:again\n'); + assert.equal(result.state.turns.length, 2); + assert.equal(JSON.stringify(result.state).includes('owner-test-token'), false); +}); + +test('a terminal event before the HTTP response remains authoritative', async t => { + const result = await execute(t, 'events-before-http'); + assert.equal(result.code, 0); + assert.equal(result.state.turns[0].outcome.status, 'completed'); +}); + +test('lost admission response with a committed lookup does not resubmit', async t => { + const result = await execute(t, 'lost-after-admission'); + assert.equal(result.code, 0); + assert.equal(result.service.requests.length, 1); + assert.ok(result.logs.some(item => item.event === 'admission_unknown')); +}); + +test('absent after lost response retries the frozen intent with the replacement origin socket', async t => { + const result = await execute(t, 'lost-before-admission', { timeoutMs: 5000 }); + assert.equal(result.code, 0); + assert.equal(result.service.requests.length, 2); + const first = result.service.requests[0].body, second = result.service.requests[1].body; + assert.notEqual(first.originSocketId, second.originSocketId); + const { originSocketId: a, ...original } = first; + const { originSocketId: b, ...retry } = second; + assert.deepEqual(retry, original); +}); + +test('idle, completed_idle result and user delivery do not finish a turn', async t => { + const result = await execute(t, 'false-terminal-only'); + assert.equal(result.code, 2); + assert.equal(result.state.turns[0].outcome, undefined); + assert.ok(result.service.observations.includes('runtime-state')); + assert.ok(result.service.observations.includes('receipt')); + assert.equal(result.chunks.join('').includes('delivery is not completion'), false); +}); + +test('missed terminal is recovered by read-only snapshots, without claiming completion or submitting follow-up', async t => { + const result = await execute(t, 'missed-terminal', { followUps: ['must not be sent'] }); + assert.equal(result.code, 2); + assert.equal(result.service.requests.length, 1); + assert.equal(result.state.turns[0].recovery.runtime.status, 'idle'); + assert.equal(result.state.turns[0].outcome, undefined); +}); + +for (const scenario of ['unknown', 'expired']) test(`${scenario} admission never generates a new execution`, async t => { + const result = await execute(t, scenario); + assert.equal(result.code, 2); + assert.equal(result.service.requests.length, 1); +}); + +test('body mismatch is a non-retryable rejection', async t => { + const result = await execute(t, 'conflict'); + assert.equal(result.code, 1); + assert.equal(result.service.requests.length, 1); + assert.equal(result.state.turns[0].outcome.code, 'CLIENT_REQUEST_BODY_MISMATCH'); +}); + +test('interrupted terminal is not success', async t => { + const result = await execute(t, 'interrupted'); + assert.equal(result.code, 1); + assert.equal(result.state.turns[0].outcome.status, 'interrupted'); +}); + +test('recover is read-only and retains request identity', async t => { + const result = await execute(t, 'normal'); + const before = result.service.requests.length; + assert.equal(await runClient({ recover: result.filename }, result.env, { log() {}, text() {} }), 2); + assert.equal(result.service.requests.length, before); + assert.equal(JSON.parse(readFileSync(result.filename, 'utf8')).turns[0].body.clientRequestId, result.state.turns[0].body.clientRequestId); +}); + +test('explicit cancellation uses the saved request identity and waits for interruption', async t => { + const result = await execute(t, 'false-terminal-only'); + assert.equal(result.code, 2); + assert.equal(await runClient({ cancel: result.filename, timeoutMs: 300, httpTimeoutMs: 200, pollMs: 20 }, result.env, { log() {}, text() {} }), 1); + assert.deepEqual(result.service.cancellations, [{ sessionId: 'session-test', agentId: 'agent-test', + conversationId: 'agent-test', clientRequestId: result.state.turns[0].body.clientRequestId }]); + assert.equal(result.service.requests.length, 1); +}); diff --git a/api/examples/agent-client/test/source-route.fixture.ts b/api/examples/agent-client/test/source-route.fixture.ts new file mode 100644 index 00000000..f1c85a84 --- /dev/null +++ b/api/examples/agent-client/test/source-route.fixture.ts @@ -0,0 +1,127 @@ +/** Copy into desirecore/packages/agent-service/src/__tests__/ as api-docs-node-client.integration.test.ts. + * Real Fastify, query routes, auth, digest, durable registry, lookup, delivery room and HTTP/Socket.IO. + * The query pipeline, follow-up runtime callback and Run snapshots are fake; no LLM/tools run. + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' +import fastify from 'fastify' +import { Server } from 'socket.io' + +const h = vi.hoisted(() => ({ io: null as any, pipeline: vi.fn(), seen: new Set() })) +vi.mock('../socket-server', () => ({ getSocketServer: () => h.io })) +vi.mock('../handlers/query-handlers', async () => ({ + ...await import('../query/client-request-registry'), executeQueryPipeline: h.pipeline, +})) +vi.mock('../session/store', () => ({ getSessionStore: () => ({ + findExistingMessageIds: async ({ messageIds }: { messageIds: string[] }) => messageIds.filter(id => h.seen.has(id)), +}) })) +vi.mock('../service-metadata', () => ({ + parseHandshakeExtensions: () => new Set(), + buildClientHandshakeResponse: (instanceId: string) => ({ + service: 'desirecore-agent-service', apiProtocolVersion: '1', serverVersion: '10.0.180', instanceId, + deploymentMode: 'external', clientCompatibility: { minimumVersion: '0.0.0', maximumVersion: null }, + capabilities: { workflow: false, mediaSignedUrl: true, pdfRendering: false }, + authentication: { kind: 'instance-token', scopes: ['owner'] }, serverTime: new Date().toISOString(), + }), +})) + +import { registerQueryRoutes } from '../routes/query-routes' +import { registerClientRequestLookupRoutes } from '../routes/client-request-lookup-routes' +import { registerClientHandshakeRoutes } from '../routes/client-handshake-routes' +import { instanceAuthOnRequest, verifySocketHandshakeAuth, resolveIdentity, clearEmbeddedInstanceToken, __resetHostedEmbeddedAuthForTest } from '../auth/instance-auth' +import { bindRendererDeliverySocket, resetRendererDeliveryRegistry } from '../handlers/renderer-delivery' +import { SocketEventSink } from '../handlers/query-event-sink' +import { claimClientRequest, commitClientRequestAttempt, markClientRequestSession, resetClientRequestRegistryForTests } from '../query/client-request-registry' +import { resolveClientRequestScope } from '../query/client-request-scope' +import { computeQueryRequestDigest } from '@desirecore/shared/query/request-digest' +import { resetConversationClearGateForTests } from '../session/conversation-clear-gate' +import { ensureInstanceIdentity } from '../instance-identity' + +describe('public Node client with real admission routes and fake query runtime', () => { + let temporary: string, app: ReturnType, env: Record + const keys = ['DESIRECORE_TEST_ROOT', 'AGENT_SERVICE_TOKEN', 'AGENT_SERVICE_MODE'] + beforeEach(async () => { + env = Object.fromEntries(keys.map(key => [key, process.env[key]])) + temporary = mkdtempSync(join(tmpdir(), 'api-docs-client-source-')) + process.env.DESIRECORE_TEST_ROOT = temporary + process.env.AGENT_SERVICE_TOKEN = 'owner-test-token' + process.env.AGENT_SERVICE_MODE = 'external' + __resetHostedEmbeddedAuthForTest(); clearEmbeddedInstanceToken() + resetClientRequestRegistryForTests(); resetConversationClearGateForTests(); resetRendererDeliveryRegistry(); h.seen.clear() + ensureInstanceIdentity() + app = fastify({ logger: false }) + app.addHook('onRequest', instanceAuthOnRequest) + await registerClientHandshakeRoutes(app) + await registerQueryRoutes(app) + await registerClientRequestLookupRoutes(app) + h.io = new Server(app.server, { path: '/socket.io/' }) + h.io.use((socket: any, next: (error?: Error) => void) => { + const decision = verifySocketHandshakeAuth(socket.handshake.auth.token) + if (!decision.ok) return next(new Error('unauthorized')) + socket.data.authKind = decision.authKind + socket.data.userId = resolveIdentity().userId + next() + }) + h.io.on('connection', (socket: any) => { + bindRendererDeliverySocket(socket) + socket.on('query:followUp', (body: any, ack: (value: any) => void) => { + const scope = resolveClientRequestScope({ authenticatedUserId: socket.data.userId, agentId: 'agent-fixture' }) + const claim = claimClientRequest(body.clientRequestId, scope, { + userMessageId: body.message.id, + requestDigest: computeQueryRequestDigest({ prompt: body.message.content, userMessageId: body.message.id }), + }) + if (claim.status !== 'claimed') return ack({ ok: false, error: claim.status }) + expect(commitClientRequestAttempt(claim.attempt).status).toBe('committed') + markClientRequestSession(body.clientRequestId, 'runtime-fixture', scope) + h.seen.add(body.message.id) + const sink = new SocketEventSink(socket) + sink.setEventRouting({ clientRequestId: body.clientRequestId, conversationId: 'agent-fixture' }) + ack({ ok: true, sessionId: 'runtime-fixture', clientRequestId: body.clientRequestId }) + socket.emit('query:followUpAccepted', { sessionId: 'runtime-fixture', clientRequestId: body.clientRequestId }) + sink.emitMessage({ sessionId: 'runtime-fixture', message: { type: 'text', text: 'fake follow-up' } }) + sink.emitComplete({ sessionId: 'runtime-fixture', terminal: { status: 'completed', runId: 'run-fixture' } }) + }) + }) + h.pipeline.mockReset() + h.pipeline.mockImplementation(async (body: any, sink: any, _context: any, attempt: any) => { + expect(commitClientRequestAttempt(attempt).status).toBe('committed') + h.seen.add(body.userMessageId) + sink.setEventRouting({ clientRequestId: body.clientRequestId, conversationId: 'agent-fixture' }) + sink.emitStarted({ sessionId: 'runtime-fixture', runId: 'run-fixture', agentId: 'agent-fixture' }) + sink.emitMessage({ sessionId: 'runtime-fixture', message: { type: 'text', text: 'fake start' } }) + sink.emitComplete({ sessionId: 'runtime-fixture', terminal: { status: 'completed', runId: 'run-fixture' } }) + return { sessionId: 'runtime-fixture' } + }) + await app.listen({ host: '127.0.0.1', port: 0 }) + }) + afterEach(async () => { + await new Promise(resolve => h.io.close(() => resolve())) + await app.close() + h.io = null; resetRendererDeliveryRegistry(); resetClientRequestRegistryForTests(); clearEmbeddedInstanceToken() + for (const key of keys) if (env[key] === undefined) delete process.env[key]; else process.env[key] = env[key] + rmSync(temporary, { recursive: true, force: true }) + }) + it('runs the CLI through real start/follow-up and durable admission lookup, without a model', async () => { + const modulePath = process.env.DESIRECORE_API_EXAMPLE_PATH + if (!modulePath) throw new Error('Set DESIRECORE_API_EXAMPLE_PATH to the public example client.mjs') + const { runClient } = await import(/* @vite-ignore */ pathToFileURL(modulePath).href) + const address = app.server.address() as { port: number } + const stateFile = join(temporary, 'request.json'), chunks: string[] = [] + const code = await runClient({ state: stateFile, prompt: 'hello', followUps: ['again'], timeoutMs: 3000, httpTimeoutMs: 1000, pollMs: 20 }, { + DESIRECORE_URL: `http://127.0.0.1:${address.port}`, DESIRECORE_TOKEN: 'owner-test-token', DESIRECORE_AGENT_ID: 'agent-fixture', + }, { log() {}, text: (value: string) => chunks.push(value) }) + expect(code).toBe(0) + expect(chunks.join('')).toBe('fake start\nfake follow-up\n') + const state = JSON.parse(readFileSync(stateFile, 'utf8')) + expect(state.turns).toHaveLength(2) + expect(state.turns.map((turn: any) => turn.outcome.status)).toEqual(['completed', 'completed']) + expect(state.turns[0].body.requestDigest).toBe(computeQueryRequestDigest({ prompt: 'hello', userMessageId: state.turns[0].userMessageId })) + expect(state.turns[0].admission.status).toBe('committed') + expect(h.pipeline).toHaveBeenCalledTimes(1) + const denied = await app.inject({ method: 'POST', url: '/api/query/start', body: state.turns[0].body }) + expect(denied.statusCode).toBe(401) + }, 15000) +}) diff --git a/api/verification.md b/api/verification.md new file mode 100644 index 00000000..40513417 --- /dev/null +++ b/api/verification.md @@ -0,0 +1,163 @@ +# API 完整实施核查与验证 + +更新日期:2026-10-15。本次在首批基础上实现全部选定公开能力;以下验证按层记录,不将本机假上游、真实服务 listener 与商业模型或安装包验收混为一项。 + +## 当前来源与范围 + +- 最终主仓合并:`e45f22902af2dc45b109cfa8f454795538aea2ed`(PR #4798),完整CI通过。原定向测试基线为 `d3bac348db5f15ce6a59469ffef8f495c84ced59` / `74058cea9`;最终差异另记,未把旧测试写成新快照实跑。 +- 最终内部文档指针:`9cc47fc9216db4a3803a982f00649f7af667c933`,已直接从远程成功获取;原核查快照 `10b6007b7e46477ee4df2e15563f09c1ab7bd1fe` 的记录保留。 +- 原有开发工作树的 `docs` 与 `package-lock.json` 修改、个人 Agent 仓库的本地提交均保留;实现使用隔离 checkout 与数据根。 +- 8 组双语页面;24 个 HTTP 操作;三个自包含 OpenAPI 3.1 子集;独立 Node.js 客户端及逐字相同的下载副本。 +- 内部盘点先覆盖79份非归档 API Markdown、817个字面量路由调用;加入新管理合同并扩展当前 Agent Service 扫描后,固定快照覆盖80份、855个 Babel AST 字面量路由调用;另核读并行新增的应用接口后台账共81份。AST仅作发现,动态路径及条件装配不能由此证明;公开24个操作另核验认证、Schema和结果方式。 + +## 接入缺口与代码修复 + +Webhook 新管理入口复用 owner 治理和 Agent 原子写入,服务解析所选实际工具及文件范围,调用方不能提交权限对象。首次 secret 与轮换值只返回一次,读取不返回 secret;触发仍使用 Agent 级共享 secret。配置在等待写锁时变化返回 `409 webhook_configuration_changed`,防止保存旧配置下的授权快照。新能力由握手 `webhookManagement` 检测,没有伪造首次客户端发行号。 + +配置并非不可修改的可信签名体系。授权 Write、Bash 或系统文件写仍遵循既有审批并可以修改有效范围内的配置;指南给出最小工具授权建议。通用 Webhook 幂等、请求级 Run ID、结果回调与自动最终回答交付未新增。 + +Compat Proxy 地址事实抽到轻量叶子模块,生命周期 owner 在监听成功/真实关闭后更新;`ComputeCredential list` 给出实际含 `/v1` 的本机 Base URL。默认 `get` 安全 token 输出和真实 Key 治理保持现行合同。 + +合并最新 `dev` 时保留上游宿主认证治理规则、本次 Webhook 管理规则和双方内部文档;没有丢弃功能逻辑。最新 query 的可选应用准入与后台工具过滤按当前基线重新校准规范和测试。 + +## 定向验证 + +| 范围 | 最终结果与边界 | +|---|---| +| 最新源码7个unit文件 | 112通过:管理12、代理地址2、ComputeCredential62、代理生命周期2、治理21、URL编码5、握手8 | +| 真实 Webhook 管理/执行 | owner认证、AgentFS原子写、生产运行时与工具执行、SendUserMessage持久交付、轮换、provider400后台失败、停用和删除;仅模型上游为回环假SSE | +| 代理安全 token | 15通过;真实本机HTTP鉴权、吊销及Provider绑定 | +| 代理协议 | 18通过、31未选择;Messages/Chat Completions转换和直通、非流式/流式、软硬截断、model错误、Models鉴权和地址生命周期 | +| Workflow | 4通过;独立真实standalone、Node code节点、Webhook→SSE→持久history,重连终态、错误凭据、缺grant与退役写入拒绝 | +| Node客户端独立合同 | 12通过;HTTP与Socket事件关联、先于响应的事件、丢确认、重连、保守恢复、内容冲突、中断和显式取消;实例与执行均为夹具 | +| Node客户端实际路由 | 真实HTTP/Socket鉴权、query Schema/digest、持久准入与lookup、renderer房间;执行runtime仍为替身 | +| 类型与依赖边界 | 主仓13个受影响TypeScript边界通过,local profile总并行3;依赖边界检查通过 | +| 内部文档 | 37模块结构检查无问题;ADR引用通过;claim检查通过,既有未锚定提示不作为本轮新增声明 | + +CAS回归使用受控writer钩子证明冲突分支无事件写入和无secret生成;不宣称实测真实写锁并发时序。真实集成验证正常原子持久写与运行交付。成功Webhook回执的工具计数仍为0,不能据此宣称完整工具调用审计已验收。 + +网站typecheck精确排除 `api/examples/agent-client/test/source-route.fixture.ts`:该维护夹具须复制到私有源码测试目录才能解析其真实内部导入,不能当作网站源码编译。独立示例的执行合同与源码路由集成分别验证。 + +## 文档、规范与发布 + +`pnpm api:check` 当前覆盖8组双语页、24操作、44 JSON示例块、3规范。规范保留实际Draft-07合同,以OpenAPI3.1的`jsonSchemaDialect`声明;没有把改写的元Schema检验算作真实OpenAPI验收。 + +最新定向回归共10文件118通过(unit112、服务integration5、Node实际路由integration1),独立客户端12及额外代理33用例分别记录。后续两次 rebase 和文档指针修正只改变 docs;接口代码与已测完整代码树一致,未重复计算测试数。 + +三份真实Redocly CLI2.63.0规范检验均0错误:Agent Service保留47条现有条件Schema告警,另外两份无告警。AJV Draft07验证51个Schema与34个media示例通过。 + +网站 `pnpm typecheck`、双语 `pnpm build`、`pnpm api:check -- --build` 通过。后者核对HTML、canonical、hreflang、sitemap及两语言的3个spec和Node示例下载副本字节。1440、1280、1024、375宽度下六个API主/新页面均无整页水平溢出;中文对话指南切换英文后仍在同页。复制按钮反馈成功,浏览器提供的剪贴板读数为空,未声称核验剪贴板内容。 + +源码 PR:[desirecore/desirecore#4798](https://github.com/desirecore/desirecore/pull/4798)。内部文档合并保留并行应用开发文档,指定gitlink已可达远程main。公开文档 PR:[desirecore/docs#72](https://github.com/desirecore/docs/pull/72),Vercel预览与CLA通过;自动部署结果待回写。CodeQL发现XML实体串联替换可重复解码,已改为单次匹配,并验证五类实体、嵌套编码和未知实体。尚未宣称生产部署成功。未验证商业模型、公网Caddy/TLS部署、已发行桌面安装包或用户业务副作用;新能力按实际服务支持条件使用。 + +--- + +# 首批接入基础与 Webhook 核查记录(2026-10-13 历史) + +核查日期:2026-10-13。公开范围为部署所有者的实例连接,以及管理员已预配置事件的 Webhook 调用参考。该记录保留来源与验证边界,不作为所有接口或客户端发行版的运行保证。 + +## 固定来源 + +| 来源 | 基线 | +|---|---| +| `desirecore` 的 `dev` | `2bb7add4497c6068a977b248106e922f9dfb196b` | +| 内部文档 | `0b84089da1f7c050ccdab6f2912b36a8fcdb8802` | +| 公开文档的变更前基线 | `34fb4a7cbc8bd2f453ee79536981d22c7448373b` | +| 验证 checkout 的 drawio 子模块 | `88ee1f5ab4d40603a8f3aaa0d9d6dc3794c6b3f0` | + +验证 checkout 使用固定主仓 SHA,子模块初始化并拉取最新代码;内部 docs 子模块比该主仓指针更新,记录在上表。复用已有 Node 依赖,Vitest 的 workspace 源码 alias 指向固定 checkout,未替换原有开发工作树或修改客户端源码。 + +客户端版本号和发行 tag 仅用作来源线索。本批没有确认功能首次发行版本,没有为原本无版本约束的部署说明新增客户端生效版本要求。页面中的 `serverVersion: "10.0.180"` 是通过 Schema 的结构示例,实际值取自服务响应。 + +## 合同核对与发现 + +| 内容 | 核对的相对源路径 | 结果 | +|---|---|---| +| 地址和所有者凭据 | `deploy/README.md`、`scripts/dev/provision-instance.ts`、`packages/agent-service/src/standalone.ts` | 使用已有实例的交付 HTTPS origin 和启动 token;不能猜容器内部端口或临时生成值去认证旧进程 | +| 公网路径 | `deploy/Caddyfile` | 默认代理 `/api/*`,不代理 `/health`;外部探活改用认证握手 | +| 握手和就绪 | `routes/client-handshake-routes.ts`、`service-metadata.ts`、`readiness.ts`、`http-server.ts`(均在 Agent Service 下) | `200` 元数据、`503 service_not_ready` 和认证检查互有独立含义;握手不签发 token | +| Webhook 请求与认证 | `packages/agent-service/src/routes/webhook-routes.ts`、`auth/instance-auth.ts`、`packages/schemas/src/agent-service/webhook.ts` | 专用 Bearer secret、封闭顶层字段、1 MiB、立即接收响应;无请求级执行 ID 和去重 | +| 事件创建 | `app/components/resources/agents/components/AgentFormEditor.tsx`、`app/lib/rjsf/adapters/agent/schema-transformer.ts`、Agent/Webhook Schema、`builtin-tools/agent-manage.ts` | 表单序列化与默认值处理没有生成必需运行权限配置的可信流程;本批不提供从零创建教程 | +| 结果交付 | `packages/agent-service/src/session/store.ts`、`scheduler/background-delivery.ts`、`routes/schedule-routes.ts`、`app/components/chat/ChatArea.tsx` | Webhook 普通最终回答不自动进入聊天;主动 SendUserMessage 交付有独立通道,不是请求级终态查询 | +| 原内部响应说明 | 内部 `功能/智能体核心/API/智能体功能扩展.md` 的 Webhook 章节 | 仍有旧 `{success,message}` 示例;公开页改用现行 `{accepted,agentId,event}`,内部校准留在后续任务 | + +## 固定源码验证 + +复用既有隔离测试,并针对实际公开示例建立一次性验证 fixture。最终通过 **111 项测试**: + +| 文件 | 最终通过数 | 验证层 | +|---|---:|---| +| `instance-auth.test.ts` | 68 | 实例认证单元测试 | +| `client-handshake-routes.test.ts` | 8 | 真实握手路由的 inject 测试 | +| `readiness.test.ts` | 4 | 就绪门单元测试 | +| `webhook-permissions.test.ts` | 9 | 事件权限 Schema 与运行时求交 | +| `webhook-state.test.ts` | 8 | 独立临时数据根的状态记录 | +| `webhook-prompt-manifest.test.ts` | 1 | 真实处理器,查询及存储替身 | +| 一次性 `webhook-public-contract.validation.test.ts` | 8 | 真实认证、secret、路由和 Schema,查询与存储为替身 | +| `server-readiness.integration.test.ts` | 1 | 真实回环 HTTPS listener 和 Socket.IO,独立数据根 | +| 一次性 `api-public-examples.validation.test.ts` | 4 | 中英文握手响应和 Webhook 请求通过固定源码 Schema | + +最终通过数不包括重跑中的重复用例。一次性 Webhook 分支 fixture 首轮有一处错误预期:测试将 `failRun` 的分类误写为错误详情文本;源码实际传固定分类 `error`。修正 fixture 后 8 项全部通过,客户端源码未修改。 + +实际执行的最终有效命令(在固定源码 checkout 内): + +```bash +npm test -- packages/agent-service/src/__tests__/instance-auth.test.ts packages/agent-service/src/routes/client-handshake-routes.test.ts packages/agent-service/src/readiness.test.ts packages/agent-service/src/__tests__/webhook-permissions.test.ts packages/agent-service/src/routes/__tests__/webhook-state.test.ts packages/agent-service/src/routes/__tests__/webhook-prompt-manifest.test.ts +npm test -- packages/agent-service/src/routes/__tests__/webhook-public-contract.validation.test.ts +npm run test:integration -- packages/agent-service/src/__tests__/server-readiness.integration.test.ts +npm test -- packages/agent-service/src/routes/__tests__/api-public-examples.validation.test.ts +``` + +执行时使用等价的 `node_modules/.bin/vitest run --project unit|integration <指定文件>`,只运行指定文件,未运行全量或多个 project。一次性 fixture 保留在本轮验证工作树,不属于已发布的客户端源码或公开仓库测试套件。 + +Webhook 分支验证覆盖:secret 错误的 `401` 在解析 body 之前返回;远程未配置 secret 的 `403`;目标不存在或未启用的 `404`;字段/事件名/畸形 JSON 的 `400`;已认证超大 body 的 `413`;`200 accepted` 无 Run ID;接收后的模拟查询失败不回写 HTTP 响应。 + +回环集成验证服务未就绪时的 HTTP/Socket.IO 拒绝,以及恢复后握手成功。它没有启动 Caddy 容器、外部公开域名或真实模型。 + +## 文档与站点验证 + +- 接入、Webhook 和排错分别由 subAgent 核查并完成双语文件,随后交叉只读审阅。未发现凭据、响应或结果范围的语义冲突。 +- `pnpm api:check`:4 组双语页面、2 个端点、16 个 JSON 代码块通过。 +- 校验器的 7 组临时正反例覆盖唯一性、非法 JSON、双语 slug/版本/章节差异和缺失构建产物;临时文件已清理。 +- `pnpm typecheck`:通过。 +- `pnpm build`:中文和英文生产构建均通过,无新增断链。构建输出有既有依赖提示;新文件尚未进入 Git 时无法推断其 sitemap 更新日期,不影响页面生成。 +- `pnpm api:check -- --build`:读取本地 HTML、canonical、hreflang 和两语言 sitemap 通过;脚本接受 package runner 的 `--` 分隔符,不访问生产网址。 +- 本地浏览器检查了 1440、1280、1024、375 像素宽度:导航折叠、API sidebar、Webhook 正文和表格阅读成立,所检查页面无整页水平溢出。 +- 从中文 Webhook 页切换语言后仍处于英文同页;代码复制按钮反馈成功。当前浏览器没有提供可核对的剪贴板文本,未把反馈等同于剪贴板内容验证。 + +## 未覆盖与后续任务 + +本批没有验证管理员从零创建事件、真实模型处理、主动用户交付或安装包内完整操作流程,不能把 111 项定向测试解释成这些流程的验收。 + +Webhook 没有稳定请求关联、幂等键、请求级终态查询或结果回调。普通最终回答不自动交付,所以参考页明确限定已预配置事件,说明接收反馈和结果观察条件。新增可信配置入口或可靠生产投递需要独立产品/代码合同。 + +本轮完成本地文档实施和验证,尚未进行 Git 发布、自动部署、生产 sitemap 和搜索收录检查。完整内部端点盘点、内部旧正文校准、其它 API 与 OpenAPI 仍按 `PLAN.md` 后续任务推进。 + +## CI 归因与依赖修正(2026-10-14) + +公开文档 PR #72 的 XML 重复解码问题已修复:真实解码函数的4组实体/嵌套编码/未知实体回归与构建检查通过,新一轮 CodeQL、CLA、Vercel 均通过。 + +源码 #4798 首轮 unit 有18失败,其中2项为新 `webhookManagement` 能力未加入 service-metadata 的完整响应期望,已仅修正测试并单文件4/4通过(提交 `74058cea9dd59ad79a02b6c17b5993ac44605b3c`);接口代码保持前述固定核查版本。本次源码定向验收因此为118+4个独立用例。 + +其余16项在CI固定第一父 `5714a1899c0cc80d26463e596f1cadfcc1dd4533` 的隔离checkout全部复现:9个显式文件、16个精确筛选用例,其他2559项只收集并跳过,无全量测试。结构化 [归因记录](./ci-base-attribution.json)保留环境、子模块、用例与失败签名;macOS/Node26环境未重建Linux CI,因此结论是基线已存在相同失败,而非所有环境等价。 + +上游 #4793 已包含这些16项的对应修正。该上游PR的剩余导航测试缺少现行允许的application-object类型,独立 [测试修正 #4801](https://github.com/desirecore/desirecore/pull/4801)补清单并验证精确ref、current pin拒绝与consumer错配拒绝。仅测试文件改动,11单文件unit及13受控类型边界通过,未修改任何Schema或授权实现。待该修正与上游合入,再更新API分支完成完整门禁和发布。 + +### 上游衔接补验 + +#4801 已合并;上游最新 `33926b9d243b4f2bd6ea36b2e7634e347ec33d6b` 的新增应用对象导航未改变本轮24端点及 query/Socket.IO/Run 契约。本地隔离衔接快照 `66ee3deb567a7bbcbe1bde2f48f352a129c39ec0`、docs `f620bc410063301cb5828f2ad63f9a10ce61f086` 上,限定11个API生产文件与旧已测版本完全一致,18项unit与1项integration补验通过。补验是重复覆盖,不增加前述122个独立源码用例计数;本地快照未推送,不将其等同PR完整门禁。 + +```bash +node_modules/.bin/vitest run --project unit --maxWorkers=1 --no-file-parallelism packages/agent-service/src/routes/webhook-management-routes.test.ts packages/agent-service/src/builtin-tools/__tests__/compute-credential-proxy-address.test.ts packages/agent-service/src/__tests__/service-metadata.test.ts +node_modules/.bin/vitest run --project integration --maxWorkers=1 --no-file-parallelism packages/agent-service/src/routes/webhook-management.integration.test.ts +``` + +公开PR #72 的CodeQL、CLA与Vercel预览全部通过;生产合并与部署仍等待源码前置PR。生产页面、下载字节、sitemap与搜索检查尚未执行。 + +## 最终合并与发布核对(2026-10-15) + +上游 #4793 与源码 #4798 均在完整门禁通过后合并。最终 #4798 HEAD `3801f4d79cd4e578b8f3e006913a0c552e3763f3` 的CI见 [run 38559639612](https://github.com/desirecore/desirecore/actions/runs/38559639612),合并提交为 `e45f22902af2dc45b109cfa8f454795538aea2ed`。 + +相对两个原测试基线,24端点汇总的33个来源/Schema文件中仅 `instance-auth.ts` 静态导入改异步导入,仍先执行同一application authority gate,再处理豁免与owner fallback。请求、响应、路由和认证分支未变。外围用量消息新可选model和混合模型回执省略身份的行为与现有指南/客户端兼容;本次只读差异核对不作为新增运行测试。公开清单及三个spec已同步固定来源,未改变API合同。 + +部署前快照:生产首页200,API中英文入口与规范下载404,两语言sitemap不含API路径。生产验收结果将在自动部署后单独回写;这些404不作为部署后结果。 diff --git a/docs/02-user-guide/09-capabilities/06-integrations-overview.md b/docs/02-user-guide/09-capabilities/06-integrations-overview.md index a718af6f..f3490572 100644 --- a/docs/02-user-guide/09-capabilities/06-integrations-overview.md +++ b/docs/02-user-guide/09-capabilities/06-integrations-overview.md @@ -27,6 +27,7 @@ DesireCore 不是一个封闭系统。它通过多层集成架构连接外部世 | **邮件** | 统一管理 Gmail / Outlook / IMAP 邮箱 | [邮件管理](../email/overview) | | **工作流** | 用可视化 DSL 编排触发器、代码、LLM、Agent 和人工确认节点 | [任务编排](../../concepts/task-orchestration) | | **应用、插件与服务** | 区分应用使用、插件贡献管理和服务连接诊断 | [应用、插件与服务](./app-service-catalog) | +| **实例 API 与 Webhook** | 外部程序验证实例连接,或向已配置的事件发送触发请求 | [集成与 API](../../07-api/index.md) | :::tip 一句话理解 内置工具是"出厂能力",MCP 是"扩展接口",技能包是"工作流模板",Computer Use 是"万能后备"——四者互补,覆盖几乎所有场景。 @@ -86,17 +87,12 @@ Computer Use 适合没有开放 API、需要跨多个桌面应用完成的任务 ## 可观测性与接口审计 -所有集成操作都有完整的审计记录,支持企业合规要求。无论是 API 调用、工具使用还是外部服务交互,DesireCore 都会记录详细的执行日志,确保每一步操作可追溯、可审计。 - -- **API 调用日志** — 记录每次外部 API 的请求和响应 -- **工具使用记录** — 追踪智能体调用了哪些工具、传入了什么参数 -- **外部服务交互追踪** — 记录与 MCP 服务、邮件服务器等的通信 -- **异常和错误报告** — 集成失败时自动进入错误日志,便于排查 - -这对于有合规需求的企业用户尤为重要——你可以随时回溯智能体的行为,了解它做了什么、为什么这样做。详见 [审计追踪](../security/audit-trail)。 +DesireCore 会记录受支持的工具调用、审批和执行信息,帮助你核对智能体请求了什么、实际执行了什么。记录的具体内容取决于工具、服务与日志设置;不能把它理解为所有接口的请求和响应都会完整保存。日志可能包含敏感信息,请按需访问和保护。详见 [审计追踪](../security/audit-trail)。 ## 自定义集成 +如果你希望自己的程序连接 DesireCore 实例,或通过外部事件触发已经配置的智能体 Webhook,参见[集成与 API](../../07-api/index.md)。Webhook 的成功响应只表示触发已接收,具体结果交付方式见对应指南。 + 如果现有集成不满足你的需求,DesireCore 提供四种方式来扩展: 1. **自建 MCP 服务** — 按照 MCP 协议规范实现自己的服务端,让智能体调用你的内部 API。参见 [MCP 集成](./mcp-integration) diff --git a/docs/05-more/index.md b/docs/05-more/index.md index cdfd8765..f589ed2d 100644 --- a/docs/05-more/index.md +++ b/docs/05-more/index.md @@ -21,3 +21,4 @@ sidebar_position: 0 | [开源仓库](./09-open-source-repos.md) | 官方 GitHub 仓库一览 | | [第三方软件与许可](./10-third-party-software.md) | 内置第三方组件及开源许可 | | [更新日志](./11-changelog/index.md) | 版本更新历史记录 | +| [集成与 API](../07-api/index.md) | 从外部程序连接实例、验证访问并调用已配置的 Webhook | diff --git a/docs/07-api/01-connection-and-auth.md b/docs/07-api/01-connection-and-auth.md new file mode 100644 index 00000000..cc486e5e --- /dev/null +++ b/docs/07-api/01-connection-and-auth.md @@ -0,0 +1,154 @@ +--- +title: 连接实例与认证 +description: 使用部署者提供的实例地址和访问令牌,验证 standalone Agent Service 连接,区分实例认证与 Webhook secret。 +slug: /api/connection-and-auth +sidebar_position: 1 +keywords: [API, standalone, Agent Service, 实例令牌, 认证, client-handshake] +--- + +# 连接实例与认证 + +部署所有者可以从自己的程序或命令行访问独立部署的 DesireCore 实例。本页以已经运行的 **standalone Agent Service** 为例,说明地址和凭据的来源,并完成一次只读连接验证。 + +## 准备地址和令牌 + +你需要两项部署信息: + +| 信息 | 用途 | 获取方式 | +|---|---|---| +| 实例对外地址 | 示例中的 `BASE_URL` | 使用该实例交付的 HTTPS 地址,包含实际使用的非标准端口 | +| 实例访问令牌 | 请求头 `X-Agent-Service-Token` | 使用启动实例时配置的 `AGENT_SERVICE_TOKEN` | + +如果使用开实例工具保存了部署清单,可以在部署项目的 `.deploy/instances/<实例名>/instance.json` 中查看 `token` 字段,地址以该实例的交付信息为准。手动启动的实例则从你保管的进程或容器启动配置中取得 `AGENT_SERVICE_TOKEN`。这是已有部署的令牌;临时生成一个新值不能认证正在运行的实例。 + +例如,对外地址为 `https://agent.example.com` 时,使用这个地址作为 `BASE_URL`;交付地址带有端口时保留端口。`BASE_URL` 不包含 `/api`、页面路径、查询参数或末尾斜杠。容器内服务端口不是对外交付地址,不能据此猜测外部调用端口。 + +`https://docs.desirecore.com/api` 是文档阅读地址。下面的请求应发往你自己的实例。 + +## 选择对应的凭据 + +| 你要做什么 | 使用的凭据 | 发送方式 | +|---|---|---| +| 以部署所有者身份调用实例 API | 实例访问令牌 | `X-Agent-Service-Token: <实例令牌>` | +| 从外部服务触发管理员已配置并验证的 Agent Webhook | 管理员提供的目标 Agent Webhook secret | `Authorization: Bearer ` | +| 让 DesireCore 连接模型供应商 | 供应商 API Key | 在[配置 API Key](../01-getting-started/04-configure-api-key.md)中设置 | + +实例访问令牌代表实例所有者的访问权限,应由部署所有者或受信任的服务保管。Webhook secret 只用于对应 Agent 的触发入口;仅需要触发事件的服务使用该 secret 即可。各接口仍有自己的配置、执行权限和审批要求。 + +桌面客户端的实例地址和凭据由其连接机制管理,本页的部署清单路径不适用于桌面客户端。桌面配对取得的客户端凭据持续有效至撤销,重启不会自动撤销;`interactive` 标记不代表严格的最小权限。配对凭据不能与所有者令牌互换,也不能直接套用下面的所有者接入流程。 + +## 验证连接 + +将下列占位值替换为你已有实例的地址和令牌,在命令行执行: + +```bash +BASE_URL='https://agent.example.com' +AGENT_SERVICE_TOKEN='replace-with-your-existing-instance-token' + +curl --silent --show-error --include \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/client-handshake" +``` + +### `GET /api/client-handshake` + +| 项目 | 说明 | +|---|---| +| 请求体 | 无 | +| 必需请求头 | `X-Agent-Service-Token` | +| 成功状态 | `200 OK`,JSON 响应 | +| 响应缓存 | `Cache-Control: no-store, private` | + +以下为响应结构示例,版本、实例标识、时间和能力值以你的实例实际响应为准: + +```json +{ + "service": "desirecore-agent-service", + "apiProtocolVersion": "1", + "serverVersion": "10.0.180", + "instanceId": "inst_0123456789abcdefghijklmnopqrstuv", + "deploymentMode": "external", + "clientCompatibility": { + "minimumVersion": "0.0.0", + "maximumVersion": null + }, + "capabilities": { + "workflow": false, + "mediaSignedUrl": true, + "pdfRendering": false, + "localPairing": true, + "queryRequestIdEcho": true + }, + "authentication": { + "kind": "instance-token", + "scopes": ["owner"] + }, + "serverTime": "2026-10-13T08:00:00.000Z" +} +``` + +确认以下字段后,再继续调用具体功能: + +| 字段 | 如何使用 | +|---|---| +| `service` | 必须为 `desirecore-agent-service`,避免连接到其它服务或网页 | +| `instanceId` | 标识该实例的数据空间;保存后可用于发现是否连到了另一个实例 | +| `deploymentMode` | `external` 和 `owner` 均为 standalone 形态;`embedded` 表示桌面实例 | +| `apiProtocolVersion` | 握手与连接协议编号,当前为字符串 `"1"` | +| `serverVersion` | 实例的软件版本,用于排查兼容问题 | +| `clientCompatibility` | DesireCore 客户端的连接兼容范围,`maximumVersion: null` 表示未设上限 | +| `capabilities` | 当前部署的能力信息;功能不可用时不能仅凭连接成功推断可用 | +| `authentication` | 部署形态的认证类型和实例作用域,具体操作还需满足相应接口的权限要求 | +| `serverTime` | 服务端响应时间,可用于排查时钟差异 | + +握手验证已有凭据和目标服务,成功后不会签发新令牌,也不代表模型服务、Agent 权限或所有可选功能都已经配置完成。客户端连接兼容范围与单个功能的生效版本分别按各自含义使用。 + +### 启动期间的响应 + +实例尚未完成启动时,带有效令牌的握手可能返回: + +```http +HTTP/1.1 503 Service Unavailable +Retry-After: 1 +Cache-Control: no-store +``` + +```json +{ + "status": "starting", + "service": "agent-service", + "code": "service_not_ready" +} +``` + +按 `Retry-After` 等待后重试这次只读握手。持续返回该响应时,检查实例的启动日志。令牌缺失或错误仍会返回 `401`,不能通过反复等待解决认证失败。 + +## 探活与连接验证 + +`GET /health` 供直连服务监听地址的探针使用,不要求实例访问令牌;它只报告服务状态。在默认 standalone 反向代理部署中,该路径没有作为对外 API 转发,外部调用使用上面的认证握手。 + +直连探针在启动未就绪时也会收到 `503 service_not_ready`;可以接收请求后,响应示例如下: + +```json +{ + "status": "ok", + "service": "agent-service", + "protocol": "http", + "fullReady": true +} +``` + +`protocol` 表示被探测的 HTTP 或 HTTPS 监听服务;`fullReady` 是后台初始化的诊断信息,可以在服务已经接收请求时仍为 `false`。探活成功不能验证实例令牌、目标实例身份或某个 Agent 的运行结果。对外地址的 `/health` 即使返回 `200`,也可能是网页 HTML,不能作为 API 连接成功依据。 + +## 保管与更新令牌 + +把实例令牌保存在受信任的服务配置中,避免写入网页源码、公开仓库、URL 或共享日志。standalone 启动日志只说明令牌已设置,不回显令牌值。 + +更新实例令牌时,修改你保管的启动配置中的 `AGENT_SERVICE_TOKEN`,重新启动或重建对应服务进程,再更新调用方配置。仍使用原配置重启不会撤销旧令牌。使用部署清单时也要同步维护其中的令牌,避免下次启动恢复旧值。Webhook secret 由管理员在目标 Agent 的 Webhook 配置中单独更新,并通知调用方替换。 + +浏览器页面调用还受 HTTPS、浏览器跨域规则以及部署者配置的允许来源影响。实例启用来源限制时,`AGENT_SERVICE_CORS_ORIGIN` 使用调用页面的精确来源,例如 `https://app.example.com`,不包含页面路径。来源获准仍需要有效凭据;不要把所有者令牌嵌入提供给其他用户的网页。 + +## 下一步 + +- [Agent Webhook](./02-agent-webhook.md):通过目标 Agent 的专用 secret 触发事件。 +- [错误与排查](./03-errors-and-troubleshooting.md):处理认证、启动、请求和异步执行问题。 diff --git a/docs/07-api/02-agent-webhook.md b/docs/07-api/02-agent-webhook.md new file mode 100644 index 00000000..9b0420e6 --- /dev/null +++ b/docs/07-api/02-agent-webhook.md @@ -0,0 +1,268 @@ +--- +title: 智能体 Webhook +description: 创建智能体 Webhook 事件、选择运行权限和轮换密钥,并从外部触发及查看主动交付。 +slug: /api/agent-webhook +--- + +# 智能体 Webhook + +外部系统可以向 DesireCore 发送一个已配置的事件,让目标智能体在后台处理事件内容。适合将业务通知、监控事件或其他应用的消息交给智能体处理。 + +实例所有者可以通过管理接口创建事件、选择工具并取得专用密钥。仅负责发送事件的外部服务,则向管理员取得已配置的事件参数和密钥。 + +先调用[认证握手](./01-connection-and-auth.md),检查 `capabilities.webhookManagement`。只有值为 `true` 的实例支持下方管理步骤;字段缺失或为 `false` 时,当前实例暂不可通过这些接口创建或修改事件。已有有效配置仍可按[触发请求](#请求)调用。 + +## 调用前准备 + +| 信息 | 获取方式与要求 | +| --- | --- | +| `BASE_URL` | 管理员提供的 Agent Service 地址;参见[连接与认证](./01-connection-and-auth.md)。公开文档站点的地址不能用于调用。 | +| `AGENT_ID` | 管理员提供的目标智能体 ID,不能用显示名称替代。该智能体必须已启用 Webhook。 | +| `event` | 管理员提供的已配置事件名称,必须完全匹配,例如 `demo.ping`。 | +| `WEBHOOK_SECRET` | 目标智能体的专用 Webhook 密钥。发送方通过自己的密钥管理方式保存和注入;需要轮换时联系管理员,并同步更新发送方。 | +| 事件处理约定 | 确认事件使用的字段、可执行操作及结果观察方式。触发请求本身不提供工具或文件权限设置;实际操作取决于已选工具和文件范围。 | + +外部调用必须配置 Webhook 密钥。远程实例未配置密钥时会拒绝触发;本机实例没有密钥时,也不能通过本页的外部调用方式触发。这份密钥与实例访问令牌、模型供应商 API Key 的用途不同;发送方应使用管理员提供的专用 Webhook 密钥。 + +发送方需要能访问实例地址。桌面实例的本机回环地址只适用于同一台电脑上的调用;外部服务器需要管理员提供可访问的服务地址。DesireCore 服务必须保持运行。 + +## 创建事件并选择运行权限 + +本节由 standalone 实例所有者操作,使用[连接与认证](./01-connection-and-auth.md)中的 `X-Agent-Service-Token`。Webhook 密钥只用于触发,不授予事件管理或回执读取权限。命令示例需要 `curl` 和 `jq`。 + +### 1. 查看事件与可选工具 + +使用已有实例地址、所有者令牌和目标智能体 ID。智能体 ID 可从[智能体列表](./04-conversations-and-runs.md)取得。下面保存的管理响应只包含密钥是否存在,不包含密钥正文。 + +```bash +BASE_URL='https://your-instance.example' +AGENT_ID='your-agent-id' +AGENT_SERVICE_TOKEN='replace-with-your-existing-instance-token' +umask 077 + +curl --fail-with-body --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/agents/${AGENT_ID}/webhooks" \ + --output webhook-config.json + +jq '{enabled,hasSecret,events,availableTools}' webhook-config.json +``` + +| 响应字段 | 说明 | +| --- | --- | +| `enabled` | 该智能体的 Webhook 入口是否启用。 | +| `hasSecret` | 是否已配置密钥;读取不会再次返回密钥。 | +| `events` | 已有事件的 `event`、`promptTemplate`、`maxTurns` 和 `toolIds`。 | +| `availableTools` | 当前可选择工具的 `id`、`name`、`riskLevel` 与 `requiresConfirmation`。 | + +选择事件实际需要的工具 ID。`toolIds: []` 表示不允许使用工具。本例选择 `SendUserMessage`,用于把测试结果主动发送到用户对话;如果它不在 `availableTools` 中,请先调整目标智能体的工具配置,再重新读取。 + +只为事件完成工作所需的操作选择工具。本例只需 `SendUserMessage`;确实需要写文件或执行命令时,再添加 `Write`、`Bash` 等工具。这些工具可以修改有效文件范围内的配置文件,包括该范围中的智能体与事件配置。工具选择和文件范围沿用实例现有的权限机制,授予写入能力时需要同时考虑它能修改哪些配置。 + +### 2. 保存 `demo.ping` + +管理接口接受事件模板、所选工具 ID 和最多执行轮数;保存时由服务确定对应工具和当前文件范围。你不需要填写工具定义或权限对象。运行时仍受目标智能体的当前权限限制。 + +| 字段 | 要求 | +| --- | --- | +| 事件名称(路径中的 `:event`) | 长度 1–128;首字符为英文字母或数字,其余可用英文字母、数字、`.`、`_`、`-`;不能为 `constructor`、`prototype` 或 `__proto__`。 | +| `promptTemplate` | 必填非空字符串,最多 32,768 个字符;支持下文的 `{{key}}` 模板字段。 | +| `toolIds` | 必填,来自 `availableTools` 的 ID 数组,不能重复;最多 512 项,空数组表示不授予工具权限。 | +| `maxTurns` | 可选整数,范围 1–100,省略时为 5。 | + +`PUT` 新建或替换同名事件,并启用该智能体的 Webhook 入口。替换会同时更新模板和工具选择;再次保存时会重新采集当前文件访问范围。请求体上限为 128 KiB。 + +如果保存返回 `409` 和 `error: "webhook_configuration_changed"`,说明智能体配置在保存期间发生变化,本次事件修改未提交。重新 `GET` 读取事件和可选工具,确认模板、工具选择及当前文件权限后,再发起保存。 + +```bash +SEND_USER_MESSAGE_TOOL_ID="$(jq -er \ + '[.availableTools[] | select(.name == "SendUserMessage")] | if length == 1 then .[0].id else error("SendUserMessage is not available") end' \ + webhook-config.json)" + +jq -n --arg tool "${SEND_USER_MESSAGE_TOOL_ID}" \ + --arg prompt '收到消息:{{message}}。请使用 SendUserMessage 向用户发送这条收到的消息。' \ + '{promptTemplate:$prompt,toolIds:[$tool],maxTurns:5}' \ + > webhook-event.json + +curl --fail-with-body --silent --show-error --request PUT \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --header 'Content-Type: application/json' \ + --data-binary @webhook-event.json \ + "${BASE_URL}/api/agents/${AGENT_ID}/webhooks/events/demo.ping" \ + --output webhook-write.json +``` + +保存成功返回 `200`: + +```json +{ + "success": true, + "enabled": true, + "event": "demo.ping", + "secret": "" +} +``` + +`secret` **仅在这次保存首次生成密钥时出现**。立即通过发送方的密钥管理方式保存这个值;不要把包含密钥的响应文件放入公开仓库或日志。已有密钥时,响应只有 `success`、`enabled` 和 `event`。 + +如果已保存的密钥丢失,或首次生成密钥的响应丢失,请按[轮换密钥](#轮换密钥)取得新值。重新 `GET` 或再次保存事件不会取回旧密钥。 + +### 3. 触发并查看主动交付 + +将生成或轮换得到的密钥设置为 `WEBHOOK_SECRET`,按下方[发送一次请求](#发送一次请求)触发 `demo.ping`,然后按[查看处理结果](#查看处理结果)确认主动消息已交付。智能体需要可用的模型配置,并且实际调用了获准的发送消息工具;保存和接收成功都不能代替这一步确认。 + +## 暂停、删除与密钥轮换 + +以下管理操作均使用实例访问令牌。它们改变之后的触发配置,不会停止已经接收的后台执行。 + +| 操作 | 请求 | 成功响应 | +| --- | --- | --- | +| 暂停全部事件 | `PATCH /api/agents/:id/webhooks`,JSON `{"enabled":false}` | `{"success":true,"enabled":false}` | +| 恢复已有事件 | 同一路径,JSON `{"enabled":true}` | `{"success":true,"enabled":true}`;需要已有事件和密钥。 | +| 删除一个事件 | `DELETE /api/agents/:id/webhooks/events/:event`,无请求体 | `{"success":true,"enabled":...}`;删除最后一个事件后自动暂停入口。 | + +### 轮换密钥 + +一个智能体的事件共用同一份 Webhook 密钥。轮换后旧密钥立即不能用于之后的触发,所有发送方都要更新;已有运行继续处理。 + +```bash +curl --fail-with-body --silent --show-error --request POST \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/agents/${AGENT_ID}/webhooks/secret/rotate" \ + --output webhook-secret.json +``` + +此请求不发送正文,成功返回 `200 {"secret":""}`。将新值保存到发送方的密钥管理配置。轮换响应丢失时,读取配置也不能取回密钥,需要再次轮换并同步全部发送方。 + +## 请求 + +```http +POST /api/webhook/:agent_id +Authorization: Bearer +Content-Type: application/json +``` + +`Authorization` 使用上面的 `Bearer ` 前缀,后面紧接密钥。请求体上限为 **1 MiB(1,048,576 字节)**;这是整个 JSON 请求的大小上限。 + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `event` | string | 是 | 目标智能体中已配置的事件名称。 | +| `payload` | object | 否 | 提供事件模板需要的字段;省略时按空对象处理。 | + +顶层只接受 `event` 和 `payload`。`payload` 内可以包含事件约定的 JSON 字段,但它必须是对象,不能直接传数组或字符串。 + +```json +{ + "event": "demo.ping", + "payload": { + "message": "测试事件:请确认已收到。" + } +} +``` + +第三方系统的原始通知需要先转换成这个结构。例如,使用 GitHub 推送事件时,发送方需要映射事件名和负载,并添加这里要求的 Bearer 密钥;本接口不直接校验 GitHub 的签名头。 + +### 模板字段 + +管理员配置的提示模板通过 `{{key}}` 读取 `payload` 中同名的顶层字段。例如,模板 `收到消息:{{message}}` 会从上面的请求得到 `收到消息:测试事件:请确认已收到。`。 + +- 字符串、数字和布尔值转换为文本;对象和数组转换为 JSON 文本。 +- 字段缺失或值为 `null` 时,原占位符保留,例如 `{{message}}`。 +- `{{user.name}}` 这样的嵌套字段路径不支持。需要使用的值应由发送方放入约定的顶层字段。 + +发送方应按事件约定填写所需字段,避免将未替换的占位符交给智能体处理。 + +## 发送一次请求 + +下面的示例使用上方创建的 `demo.ping`,或管理员已经配置的同名事件,并约定使用 `payload.message`。替换实例地址和智能体 ID,通过发送方的密钥管理方式设置 `WEBHOOK_SECRET`。如果实际事件名称或字段不同,同步修改 JSON 请求体。 + +```bash +BASE_URL='https://your-instance.example' +AGENT_ID='your-agent-id' + +curl --include --request POST \ + "${BASE_URL%/}/api/webhook/${AGENT_ID}" \ + --header "Authorization: Bearer ${WEBHOOK_SECRET}" \ + --header 'Content-Type: application/json' \ + --data '{"event":"demo.ping","payload":{"message":"测试事件:请确认已收到。"}}' +``` + +正常接收时返回 `200 OK`: + +```json +{ + "accepted": true, + "agentId": "your-agent-id", + "event": "demo.ping" +} +``` + +`accepted: true` 表示事件已接收,后台处理随后进行。它不代表模型已启动、工具调用成功或任务已经完成。响应没有 Run ID、请求 ID 或最终回答;后台失败也不会将这次已经返回的 `200` 改成错误响应。 + +## 查看处理结果 + +调用前,让管理员明确这个事件预期如何交付结果。如果事件获准使用 **SendUserMessage(向用户发送消息)**,并在运行中调用了它,已成功写入的消息或附件会出现在与该实例连接的 DesireCore 中,目标智能体的对话里。重新进入目标智能体的对话可以读取已有交付。 + +普通的模型最终回答不会自动变成这里的用户交付。没有看到消息可能是事件尚在处理、事件没有发送消息、权限或交付失败,不能单凭界面没有消息判断任务成败。已有交付也不代表该次运行的后续操作全部完成。 + +实例所有者也可以读取持久交付记录: + +```bash +curl --fail-with-body --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/agents/${AGENT_ID}/schedule-messages?limit=20&offset=0" +``` + +响应为 `{"items":[...],"total":...,"limit":20,"offset":0}`。Webhook 交付条目的 `origin` 为 `webhook`、`displayName` 为事件名,正文通常在 `delivery.message`;附件引用在 `delivery.attachments`。降级为纯文本的交付记录使用 `text`。只返回当前身份有权读取的条目。默认 `limit` 为 50,最大 200,`offset` 从 0 开始。 + +记录是该智能体的交付列表,并非按本次触发查询的终态。相同事件多次触发的记录可能混在一起,列表为空也不代表执行失败。需要区分业务事件时,可在约定负载和交付正文中携带发送方的业务编号;它不会使触发接口具备去重或唯一运行查询能力。 + +如果该事件未提供用户交付,本页调用方只能取得 `accepted` 接收反馈,执行情况需要管理员查看日志。本接口没有提供按本次请求查询完成状态或最终结果的方式。需要核实失败或超时时,请把目标智能体 ID、事件名称、发送时间和 HTTP 响应交给实例管理员,由管理员检查服务运行日志。不要在排查记录中包含密钥。 + +如果业务要求每次调用都取得唯一执行编号、可轮询的终态或结果回调,请先与管理员确认是否有适合该需求的接入方式。 + +## 重复调用与超时 + +本接口没有幂等键或请求去重功能。相同请求发送两次,可能触发两次执行;在 `payload` 中放入业务事件 ID 也不会自动启用去重。 + +发送方超时或连接中断时,接收结果可能未知:服务有可能已经接收事件并开始执行。先保存发送时间、事件名称和可用的响应信息,再核实处理情况。对于会产生外部操作的事件,不应仅因没有收到响应就无条件重发。 + +后台处理有自己的执行超时控制,失败或超时可能发生在接收成功之后。延长发送方的 HTTP 等待时间不能让这个异步接口返回最终结果。 + +## 管理接口错误 + +管理接口使用 `error` 中的稳定标识;字段校验失败可能还返回 `details`。 + +| HTTP 状态 | `error` | 处理方法 | +| --- | --- | --- | +| `400` | `invalid_webhook_management_request` | 核对必填字段、类型和限制;轮换请求不要发送正文。 | +| `400` | `invalid_webhook_event` | 使用符合事件命名规则的名称。 | +| `400` | `webhook_tool_unavailable` | 重新读取 `availableTools`,只选择仍可用的准确 ID。 | +| `400` | `webhook_events_required` | 恢复前先保存事件并确认已有密钥。 | +| `400` | `invalid_agent_id` | 核对目标智能体 ID。 | +| `401` / `403` | 认证或治理拒绝,或 `webhook_owner_required` | 使用目标实例所有者的访问令牌;Webhook 密钥不能用于管理。 | +| `404` | `agent_not_found` / `webhook_event_not_found` | 核对目标智能体与待删除事件。 | +| `409` | `webhook_configuration_changed` | 本次保存未提交;重新读取配置和可选工具,确认权限后重试保存。 | +| `409` | `webhook_configuration_invalid` | 请实例管理员修复已有配置后再保存。 | +| `413` | 保存请求体过大 | 缩小整个事件保存请求到 128 KiB 以内。 | +| `500` | `webhook_management_failed` | 请管理员查看服务日志,核实修改是否生效后再处理。 | + +## 触发接口错误 + +优先检查 HTTP 状态和实际返回的 `error`。部分错误还带有 `message` 或 `details`;不要按固定的 `success/data/error` 包装解析所有响应。 + +| HTTP 状态 | 错误或现象 | 调用方操作 | +| --- | --- | --- | +| `400` | `Invalid request body`,可能带校验 `details` | 确认 JSON 顶层为对象、`event` 为字符串、`payload` 为对象,移除其他顶层字段。 | +| `400` | `Unknown event type: ...` | 核对管理员提供的事件名称、大小写和目标智能体。 | +| `400` | JSON 无法解析 | 修正 JSON 语法后重新发送。 | +| `401` | `Invalid webhook secret` | 检查 `Authorization: Bearer ...` 和当前密钥,确认发送方已同步管理员的密钥轮换。 | +| `401` | `Invalid embedded instance token` | 该本机事件没有外部调用所需的密钥;请管理员提供已配置密钥的事件。 | +| `403` | `webhook_secret_required` | 远程实例上的目标智能体没有有效 Webhook 密钥;请管理员完成配置。 | +| `404` | `Agent not found` | 核对实例地址与目标智能体 ID。 | +| `404` | `Webhook not enabled for this agent` | 请管理员确认目标智能体的事件已启用。 | +| `413` | 请求体过大 | 缩小整个 JSON 请求到 1 MiB 以内,按事件约定减少负载。 | +| `415` | 请求媒体类型不支持 | 使用 `Content-Type: application/json`。 | +| `500` | `Failed to parse agent.json` | 请管理员检查目标智能体配置;反复重发不能修复配置。 | + +反向代理和服务连接故障还可能返回其他错误,参见[错误与排查](./03-errors-and-troubleshooting.md)。修复明确拒绝接收的请求后再发送;响应丢失的情况按上面的未知结果规则处理。 diff --git a/docs/07-api/03-errors-and-troubleshooting.md b/docs/07-api/03-errors-and-troubleshooting.md new file mode 100644 index 00000000..f5bcc1d6 --- /dev/null +++ b/docs/07-api/03-errors-and-troubleshooting.md @@ -0,0 +1,105 @@ +--- +title: 错误与排查 +description: 区分连接、认证、请求校验与后台执行失败,按实际响应处理 standalone 接入和 Agent Webhook 问题。 +slug: /api/errors-and-troubleshooting +sidebar_position: 3 +keywords: [API, 错误, 排查, Webhook, 401, 403, 503] +--- + +# 错误与排查 + +调用失败时,先检查 HTTP 状态、`Content-Type` 和响应正文。实例认证、启动状态、Webhook 处理和反向代理的错误格式不同,调用方需要保留实际响应,不能假设每个错误都有相同的 `code` 字段。 + +## 连接不上或收到网页 + +| 现象 | 检查与处理 | +|---|---| +| 域名无法解析、连接被拒或超时 | 确认使用实例的对外 HTTPS 地址和实际端口,检查实例进程、网络与反向代理状态 | +| TLS 证书验证失败 | 核对域名和证书信任链;本地测试证书须在调用环境中受信任 | +| 返回 `200`,正文却是 HTML | 检查地址与路径。使用 `${BASE_URL}/api/client-handshake` 验证,并确认 JSON 中的 `service` 为 `desirecore-agent-service` | +| 对外 `/health` 返回 HTML | 默认 standalone 部署没有将 `/health` 转发为外部 API;使用带令牌的认证握手 | +| 普通 API 请求返回 `404` | 核对方法、路径和目标部署支持范围;使用对外地址,避免误连容器内的其它监听端口 | + +浏览器打开实例页面和 curl 请求 API 是两个不同的检查。页面能打开时,API 仍可能因代理、凭据或服务启动状态失败。 + +## 实例认证与启动错误 + +以下错误适用于[连接实例与认证](./01-connection-and-auth.md)中的认证握手。 + +| HTTP 状态 | 正文中的标识 | 含义与处理 | +|---|---|---| +| `401` | `error: "unauthorized"` | `X-Agent-Service-Token` 缺失或无效。确认令牌来自目标实例,并检查调用方是否仍使用轮换前的值 | +| `403` | `error: "origin_not_allowed"` | 调用页面来源未获部署允许。由部署者核对 `AGENT_SERVICE_CORS_ORIGIN` 中的精确来源;它不替代令牌 | +| `503` | `code: "service_not_ready"` | 服务正在启动。按 `Retry-After` 等待后重试只读握手;持续失败时检查启动日志 | +| `503` | `error: "instance_token_uninitialized"` | 实例访问令牌尚未初始化。部署者检查启动配置中的 `AGENT_SERVICE_TOKEN`;正常 standalone 启动要求该值 | +| `500` | 提示实例身份文件无法读取 | 部署者检查数据目录的权限和结构;保留原有实例数据,不通过删除身份文件尝试恢复 | + +实例认证失败示例: + +```json +{ + "error": "unauthorized", + "message": "缺少或无效的实例访问令牌,请在请求头 X-Agent-Service-Token 中提供。" +} +``` + +启动未就绪示例: + +```json +{ + "status": "starting", + "service": "agent-service", + "code": "service_not_ready" +} +``` + +启动与认证分别处理:错误令牌可能在启动期间就返回 `401`。只有确认为 `service_not_ready`,才按启动等待处理。浏览器的跨域限制还可能让脚本无法读取响应,应同时检查浏览器网络面板中的请求和预检结果。 + +## Webhook 请求错误 + +外部 [Agent Webhook](./02-agent-webhook.md) 使用管理员提供的目标 Agent secret,按 `Authorization: Bearer ` 发送。调用前需要已配置并启用该事件;不要把模型供应商 Key 或另一个 Agent 的 secret 用于这个入口。 + +| HTTP 状态 | 正文中的 `error` 或现象 | 含义与处理 | +|---|---|---| +| `401` | `Invalid webhook secret` | secret 缺失或不匹配。检查目标 Agent 和 `Authorization`,使用大小写与空格均正确的 `Bearer ` | +| `403` | `webhook_secret_required` | standalone 的目标 Webhook 没有配置有效 secret。请管理员检查并配置该 Agent 的专用 secret | +| `404` | `Agent not found` | 指定的 Agent 不存在;核对目标实例和 Agent ID | +| `404` | `Webhook not enabled for this agent` | 目标 Agent 的 Webhook 未启用;请管理员确认该事件已启用后再发送 | +| `400` | `Invalid request body` | 请求不符合字段要求。根据响应的 `details` 和端点参数说明修正请求 | +| `400` | `Unknown event type: ...` | `event` 没有对应配置;使用管理员交付的准确事件名 | +| `400` | JSON 解析错误 | 修正 JSON 语法,例如去除注释、尾逗号或未转义字符 | +| `413` | 请求体过大 | Webhook 请求体超过 `1 MiB`;按编码后的字节数缩小整个请求体,包括 `event` 和 `payload` | +| `415` | 不支持请求媒体类型 | 按端点示例使用 `Content-Type: application/json` | +| `500` | `Failed to parse agent.json` | 目标 Agent 配置无法解析;请管理员修正配置中的 JSON 错误 | + +Webhook secret 校验失败示例: + +```json +{ + "error": "Invalid webhook secret" +} +``` + +参数校验失败时,`Invalid request body` 响应的 `details` 包含校验信息。JSON 解析、媒体类型和体积限制由请求处理框架检查,它们的正文可能使用 `statusCode`、`error`、`message` 等字段。反向代理也可能直接返回文本或 HTML,应先确认响应类型,再读取对应正文。 + +## 已接收触发,仍没有结果 + +Webhook 的 `200` 和 `accepted: true` 表示已接收触发。响应中没有 Run ID 或最终结果;后台执行失败不会回写这次 HTTP 响应。 + +请管理员核对该事件是否匹配了预期的 prompt、是否具备执行权限,以及流程是否配置了结果交付。普通最终回答不会自动出现在聊天中,也不会成为 Webhook HTTP 响应。需要对话通知时,由管理员为流程安排并授权发送消息步骤;发送成功的消息出现在目标 Agent 的对话中。没有收到消息不能证明事件未执行、失败或已经完成。进一步观察方法见 [Agent Webhook](./02-agent-webhook.md),后台失败需管理员检查实例执行日志。 + +请求超时或连接中断时,发送方可能不知道服务是否已经接收。当前 Webhook 没有公开幂等键,重发可能产生另一次执行。先对照已交付消息,并请管理员核对执行日志,再决定是否重发;不要把 Webhook 配置成无条件自动重试。只读握手可以按启动响应中的 `Retry-After` 重试。 + +## 提供排查信息 + +求助时记录请求时间、HTTP 方法和路径、状态码、响应类型、脱敏正文、握手中的 `serverVersion` 与 `instanceId`,以及是否在更新令牌后出现问题。提供日志片段前移除实例令牌、Webhook secret、供应商 Key 和业务数据。 + +## Webhook 配置冲突与权限 + +配置管理需要实例所有者访问权限,并要求握手返回 `capabilities.webhookManagement: true`。管理请求使用实例令牌;触发请求使用 Webhook secret。参见 [Webhook 配置步骤](./02-agent-webhook.md)。 + +`409 webhook_configuration_changed` 表示配置期间 Agent 的权限或其他配置发生变化。重新读取可用工具和现有事件,核对权限后再保存。`400 webhook_tool_unavailable` 表示工具 ID 当前不存在或无法唯一解析;重新从管理 GET 的 `availableTools` 选择。`409 webhook_configuration_invalid` 表示现有配置结构损坏,需要管理员修正。 + +## 对话、代理与工作流排查 + +对话超时后先使用示例的 `--recover` 读取准入记录;保留原请求 ID、摘要和状态文件,避免将未知状态当作未执行。详见[对话与 Run](./04-conversations-and-runs.md)。模型代理应核对本机 Base URL 和安全 token 的供应商绑定;Workflow Service 应核对独立地址和 `X-Workflow-Service-Token`,再按[工作流指南](./06-workflow-integration.md)检查 `runId` 与终态。 diff --git a/docs/07-api/04-conversations-and-runs.md b/docs/07-api/04-conversations-and-runs.md new file mode 100644 index 00000000..d4ab6a97 --- /dev/null +++ b/docs/07-api/04-conversations-and-runs.md @@ -0,0 +1,425 @@ +--- +title: 对话与运行记录 +description: 通过 HTTP 与 Socket.IO 发起和续接对话,查询智能体、Run 与回执,并处理接纳未知、重试和断线对账。 +slug: /api/conversations-and-runs +sidebar_position: 4 +keywords: [API, 智能体, Run, Receipt, 对话, 运行状态, 分页] +--- + +# 对话与运行记录 + +部署所有者可以查询自己的 standalone 实例中的智能体和运行记录,用于选择目标智能体、观察已知 Run 和读取执行回执。地址与实例令牌的取得方式见[连接实例与认证](./01-connection-and-auth.md)。以下只读请求均使用 `X-Agent-Service-Token`。 + +## 读取范围 + +智能体列表、Run 列表、Run 详情和运行态供实例所有者读取实例数据,不能作为多个租户之间逐用户隔离的接口。回执读取另核对服务端解析的可信用户身份与 Run 的用户归属;Run 不存在或不属于该用户时均返回 `404`。 + +Agent ID、Run ID、`user_id` 和 `conversation_id` 是标识信息,不是访问凭据。请求中自报用户 ID、知道 Run ID 或曾在别处看到一条记录,都不能代替认证与对应接口的归属检查。 + +## 查询智能体 {#list-agents} + +### `GET /api/agents` + +无请求体,不提供分页或筛选参数。成功响应为 `{ "agents": [...] }`;列表项是摘要,不是完整智能体配置。 + +```bash +curl --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/agents" +``` + +响应结构示例: + +```json +{ + "agents": [ + { + "id": "your-agent-id", + "name": "Example Agent", + "skillsCount": 0, + "toolsCount": 0, + "heartbeatEnabled": false, + "hasGit": false, + "isCoreAgent": false, + "status": "idle" + } + ] +} +``` + +| 字段 | 类型与含义 | +|---|---| +| `id`、`name` | 必需字符串;调用其它接口时使用 `id`,不要用显示名称替代 | +| `skillsCount`、`toolsCount` | 必需非负数;摘要中的技能和工具数量 | +| `heartbeatEnabled`、`hasGit`、`isCoreAgent` | 必需布尔值;心跳、Git 仓库与核心智能体标记 | +| `status` | 可选,`online`、`busy`、`idle`、`offline`、`error`、`recovery`、`shutdown` | +| `metadataError` | 可选,`missing`、`unreadable`、`invalid_json`、`invalid_shape`;有此字段时名称、描述和计数可能是降级值,应先请管理员修复配置 | +| `description`、`engine`、`hasRemote`、`syncStatus` | 可选描述、运行引擎、上游仓库和同步信息 | +| `ui`、`avatar`、`avatarImage` | 可选界面与头像摘要;头像配置存在不等于对应文件可用 | +| `unread`、`pinned`、`pinnedAt`、`folded`、`groupOverride`、`nestMembers` | 可选的当前实例用户未读信息与显示偏好,缺席时不要自行补成已观测值 | + +## 查询 Run 列表 {#list-runs} + +### `GET /api/runs` + +Run 列表按 `started_at` 从新到旧排序,默认不包含已清除对话的记录。筛选先作用于全部可读取记录,再取最多 `limit` 条;同时提供多个筛选条件时按 AND 匹配。 + +| 查询参数 | 类型 | 默认与含义 | +|---|---|---| +| `limit` | 单个十进制正整数字符串 | 默认 `50`;转换后必须是安全整数,不设置额外统一条数上限 | +| `agent_id` | string | 可选;执行该 Run 的智能体 ID | +| `delegated_by` | string | 可选;发起委派的智能体 ID,对应 `delegation.fromAgentId` | +| `parent_run_id` | string | 可选;委派父 Run ID,对应 `delegation.parentRunId` | + +此端点不提供 `offset`、游标或 `total`。不要套用其它接口的分页字段。`limit` 的空值、负数、小数、混杂字符串、重复参数或超出安全整数范围的值会返回 `400`。 + +先从智能体列表取得 `AGENT_ID`,再按目标智能体查询: + +```bash +AGENT_ID='your-agent-id' + +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode "agent_id=${AGENT_ID}" \ + --data-urlencode 'limit=20' \ + "${BASE_URL}/api/runs" +``` + +```json +{ + "runs": [ + { + "run_id": "00000000-0000-4000-8000-000000000001", + "user_id": "example-owner", + "agent_id": "your-agent-id", + "started_at": "2026-10-14T01:00:00.000Z", + "status": "idle" + } + ] +} +``` + +`runs` 中的每项包含下节的 Run 元信息。列表不是所有实时事件的重放,也没有针对某次 Webhook 请求的完成关联。 + +## 查询 Run 详情 {#get-run} + +### `GET /api/runs/:id` + +`:id` 使用已取得的 Run ID,无请求体和额外查询参数。成功时直接返回 Run 元信息对象,不再套一层 `run` 或 `data`。 + +```bash +RUN_ID='replace-with-an-existing-run-id' + +curl --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/runs/${RUN_ID}" +``` + +```json +{ + "run_id": "00000000-0000-4000-8000-000000000001", + "user_id": "example-owner", + "agent_id": "your-agent-id", + "started_at": "2026-10-14T01:00:00.000Z", + "status": "idle" +} +``` + +| 字段 | 含义 | +|---|---| +| `run_id`、`user_id`、`agent_id` | 必需字符串;Run、发起用户与执行智能体的标识 | +| `started_at` | 必需 ISO 8601 时间,表示 Run 首次创建时间;同一 Run 可跨多轮复用 | +| `status` | 必需,`running`、`idle`、`completed`、`failed` | +| `conversation_id`、`session_id` | 可选,对话与实时运行会话标识;不要与一次提交的请求 ID 混用 | +| `turn_started_at`、`completed_at`、`updatedAt` | 可选,当前轮次开始、Run 完成和最近活动时间 | +| `failure_reason` | 失败原因可能为 `process_restart`、`timeout`、`stale`、`error` | +| `interruption_reason`、`interrupted_at` | 可选,中止原因和时间;用户停止、系统休眠、后台超时等原因有各自标识 | +| `delegation` | 可选委派来源,含 `fromAgentId` 及可能存在的 `parentRunId`、`parentClientRequestId`、`mode` | +| `background`、`inbound` | 可选当前轮次身份信息,分别描述后台处理和来自另一个智能体的消息 | +| `title`、`messageCount`、`archived_at`、`cleared` | 可选标题、消息数量和历史生命周期信息 | +| `team_id`、`work_context_id`、`execution_attachment` | 可选执行归属信息;读取这些字段不授予相应资源权限 | + +响应还可能包含上下文、恢复和工作目录等可选元信息。缺席字段表示没有这项记录;不要据此补造完成时间、失败原因或执行归属。 + +`idle` 表示当前未在执行且可以等待后续对话,不等于整项业务已经成功。中止后的 Run 也可能回到 `idle`。`completed`、`failed` 是 Run 的生命周期状态,判断某次提交仍应关联该轮的终态和回执。 + +部分历史分支可在列表中出现,而此详情端点返回 `404`。需要判断运行态或读取回执时,分别使用下面的端点,并按它们自己的读取结果处理。 + +## 查询运行态 {#get-run-runtime-state} + +### `GET /api/runs/:id/runtime-state` + +返回 Run 的持久运行态快照,兼容历史分支。`:id` 仅允许字母、数字、下划线和连字符。成功响应只包含三个字段: + +```bash +curl --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/runs/${RUN_ID}/runtime-state" +``` + +```json +{ + "run_id": "00000000-0000-4000-8000-000000000001", + "agent_id": "your-agent-id", + "status": "idle" +} +``` + +`status` 只接受 `running`、`idle`、`completed`、`failed`。状态不能读取或不在支持范围时返回 `404`。`running` 不会被改成 `idle`,但这仍是一份快照;它不授权取消、回滚或其它修改操作,也不保证接下来提交时状态未变化。 + +## 读取执行回执 {#list-run-receipts} + +### `GET /api/runs/:id/receipt` + +回执包含每轮的输入、输出、工具调用和结果。接口根据实例凭据解析可信用户,不接收用来替代身份检查的 `user_id` 参数。Run 不属于该用户时返回 `404`,即使其它读取接口已经返回过该 Run 的元信息。 + +不带 `offset`、`limit` 时,响应为 `{ "run_id": "...", "receipts": [...] }`,按写入顺序读取已有回执。建议用分页控制每次读取量:只要提供任一分页参数,就启用以下规则。 + +| 查询参数 | 默认值 | 有效范围与处理 | +|---|---|---| +| `offset` | `0` | 单个非负十进制整数字符串;从最新回执向前跳过,超过 `10000` 时按 `10000` 处理 | +| `limit` | `20` | 单个正十进制整数字符串;超过 `100` 时按 `100` 处理 | + +两项转换前后均要有效,空值、小数、负数、重复参数、混杂字符串和非安全整数返回 `400`。响应的 `offset`、`limit` 是实际生效值。页内回执仍按写入顺序排列,最新一页中的最后一项是这一页较新的回执。 + +```bash +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode 'offset=0' \ + --data-urlencode 'limit=20' \ + "${BASE_URL}/api/runs/${RUN_ID}/receipt" +``` + +```json +{ + "run_id": "00000000-0000-4000-8000-000000000001", + "receipts": [ + { + "receipt_id": "example-receipt-id", + "run_id": "00000000-0000-4000-8000-000000000001", + "goal": "Reply to the test message", + "input": "Please say hello.", + "output": "Hello.", + "tool_calls": [], + "outcome": { "status": "success", "auto_completed": true }, + "started_at": "2026-10-14T01:00:00.000Z", + "completed_at": "2026-10-14T01:00:01.000Z" + } + ], + "total": 1, + "offset": 0, + "limit": 20, + "has_more": false, + "skipped": 0 +} +``` + +| 字段 | 使用方式 | +|---|---| +| `receipt_id` | 单条回执的稳定标识;同一 Run 可以有多条回执 | +| `goal`、`input`、`output`、`tool_calls` | 执行目标、输入、输出和工具调用;这些内容可能包含业务数据 | +| `outcome.status` | `success`、`partial`、`failed`;描述该回执记录的结果 | +| `outcome.auto_completed` | 是否无人工干预完成 | +| `started_at`、`completed_at` | 该回执的开始和完成时间 | +| `usage`、`duration_ms`、`interruption_reason` | 可选用量、耗时和中止信息;未回报的字段不能当作零用量或正常完成 | +| `total`、`has_more` | 分页读取的计数和是否还有更早记录;不带分页参数时不返回 | +| `skipped` | 分页读取时跳过的损坏或过大记录数,不表示业务执行失败次数 | + +旧记录还可能在 Schema 校验时被跳过,因此 `receipts.length` 可能小于这页跨过的记录数,`total` 也不能当成返回数组长度。继续读取更早页时按响应的 `offset + limit` 推进,不按 `receipts.length` 推进;到达 `10000` 偏移上限后停止翻页,避免重复读取同一页。 + +空回执列表、跳过的历史记录或最近一条旧回执都不能独立证明当前轮次已完成。用回执做对账时,应同时核对目标 Run、该轮时间与可取得的轮次关联信息;没有足够证据时保留结果未知。 + +## 只读接口错误 {#read-errors} + +| 端点或范围 | HTTP 状态与正文 | 处理方式 | +|---|---|---| +| 所有接口 | `401`,`error: "unauthorized"` | 核对实例令牌与目标地址 | +| 所有接口 | `403 origin_not_allowed` 或 `503 service_not_ready` | 按[错误与排查](./03-errors-and-troubleshooting.md)处理来源限制或启动等待 | +| Run 列表与分页回执 | `400`,`error` 以 `Invalid pagination:` 开头 | 使用单个符合要求的十进制整数参数 | +| Run 详情 | `404`,`error: "Run not found: ..."` | 核对 Run ID 和详情可读范围 | +| 运行态与回执 | `400`,`error: "Invalid run ID"` | 使用服务返回的安全 Run ID,不拼接路径 | +| 运行态 | `404`,`error: "Run runtime state not found: ..."` | 无法取得支持的运行态;不要默认成 `idle` | +| 回执 | `401`,`error: "无法解析用户身份"` | 请管理员检查实例身份配置 | +| 回执 | `404`,`error: "Run not found: ..."` | Run 不存在、不可读取或不属于可信用户;不能据此跳过归属检查 | +| 读取失败 | `500` | 保存脱敏响应,请管理员检查服务日志;错误正文以实际返回为准 | + +查询参数不改变执行权限。只读接口允许再次查询来更新观察结果;不要把一次失败或缺失记录自动转成重新提交任务。 + +## 发起与续接对话 {#submit-conversation} + +对话通过 HTTP 提交,通过 Socket.IO 接收进度和该轮终态。使用已经配置好模型与执行权限的目标智能体;HTTP 和 Socket.IO 必须连接同一实例,使用同一主体的实例令牌。 + +### 运行 Node.js 示例 {#node-client-example} + +[下载客户端示例](pathname:///api-examples/agent-client.mjs),保存为 `agent-client.mjs`。示例演示文本提交、顺序续问、持久保存请求信息、断线对账与显式取消。使用 **Node.js 22+**,在保存示例的独立目录安装其依赖: + +```bash +npm init -y +npm install socket.io-client@4.8.4 +``` + +填入你自己的实例地址、已有令牌和从列表取得的智能体 ID: + +```bash +export DESIRECORE_URL='https://agent.example.com' +export DESIRECORE_TOKEN='replace-with-your-existing-instance-token' +export DESIRECORE_AGENT_ID='your-agent-id' + +node agent-client.mjs \ + --prompt '请回复一句问候。' \ + --follow-up '请再用英文回复。' \ + --state ./request.json +``` + +`--follow-up` 可以重复提供,示例只在上一轮得到明确完成信息、接纳查询确认会话属于当前服务进程后尝试续问。首次调用不覆盖已有状态文件;为独立的新任务使用新的 `--state` 路径。状态文件保存原请求、业务文本与关联信息,不保存实例令牌,应按业务数据保管。 + +程序先验证握手,再注册事件监听并建立 Socket.IO 连接。连接路径为 `/socket.io/`,认证为 `auth: { token, clientInstanceId }`;`token` 与 HTTP 请求的 `X-Agent-Service-Token` 相同。`clientInstanceId` 使用稳定的 16—128 字符标识,只包含字母、数字、`.`、`_`、`:`、`-`。认证连接会绑定相应客户端接收通道,示例无需另发订阅消息。 + +### HTTP 提交字段 + +`POST /api/query/start` 使用 JSON 请求。请求顶层与 `options` 都不接受未声明字段;至少提供非空 `prompt` 或有效 `mediaRefs`。上面的文本示例使用以下字段: + +| 字段 | 用途与示例约定 | +|---|---| +| `prompt` | 本轮用户文本;示例发送非空字符串 | +| `options.agentId` | 目标智能体 ID | +| `options.conversationId` | 对话范围;示例为其保存稳定值,后续请求沿用该范围 | +| `clientRequestId` | 一次提交的稳定 ID;同一请求恢复和有限重发时保留,新一轮使用新 ID | +| `userMessageId` | 本轮用户消息的稳定 ID,与请求一起保存 | +| `requestDigest` | `qrd1:` 开头的请求内容摘要,由示例按协议计算;它不是认证凭据 | +| `originSocketId` | 当前已认证连接的 `socket.id`;重连后更新,不使用旧连接 ID | + +可靠关联与恢复需要以上稳定标识。完整请求字段与约束见[OpenAPI 下载](./07-openapi.md);不要把未列出的权限对象或用户身份字段写入请求来尝试扩大授权。 + +新请求接纳时返回 `202`: + +```json +{ + "ok": true, + "clientRequestId": "00000000-0000-4000-8000-000000000002" +} +``` + +重复接纳的响应还可能包含已有 `sessionId`。`202` 表示本次准入已接纳;它不证明用户消息已经持久提交、模型已经执行或该轮已经完成。`query:started` 等事件可能先于 HTTP 响应到达,应在提交前注册监听,按请求 ID 关联两条通道。 + +`POST /api/query/follow-up` 续接已知运行会话,使用与 start 不同的请求形状: + +| 字段 | 用途与示例约定 | +|---|---| +| `sessionId` | 服务返回的可继续使用的运行会话 ID | +| `originSocketId` | 当前已认证 Socket.IO 连接 ID | +| `message.id` | 这一轮的稳定用户消息 ID | +| `message.role` | `"user"` | +| `message.content` | 本轮文本 | +| `message.timestamp` | 用户消息时间,毫秒时间戳 | +| `clientRequestId` | 新一轮的稳定请求 ID | +| `requestDigest` | 示例计算并保存的该轮 `qrd1:` 内容摘要 | + +续问接纳响应为 `202`,包含 `ok: true`、`sessionId` 和相应 `clientRequestId`;重复接纳时可能带 `duplicate`。不要把 start 的 `prompt/options` 对象原样当成 follow-up 请求。 + +### 接收事件并判定该轮结束 {#conversation-events} + +| Socket.IO 事件 | 主要字段 | 处理方式 | +|---|---|---| +| `query:started` | `sessionId`,可能有 `runId`、`agentId`、`conversationId`、`clientRequestId` | 保存服务分配的运行标识,关联对应提交 | +| `query:followUpAccepted` | `sessionId`、可能有 `clientRequestId` | 关联续问的接纳信息,不作为完成信号 | +| `query:message` | `sessionId`、`message`,可能有请求与对话标识 | 观察文本、工具或交付等消息;`message` 结构依消息类型变化 | +| `query:complete` | `sessionId`,可能有 `clientRequestId`、`terminal` 和 `result` | 关联本轮请求后读取结构化 `terminal`;没有终态信息时保留未知 | +| `query:error` | `error`,可能有 `code`、请求和运行标识 | 保存可关联的错误;不要把未关联的错误归给另一次请求 | +| `activity:run_status` | `runId`、`status`,可能有 `sessionId` | 更新 Run 状态观察;它不是单次提交的终态 | + +`query:complete.terminal.status` 为 `completed` 时,该轮正常收口;为 `interrupted` 时,该轮被中止,可继续读取其 `reason`、`partial`、`runId` 等已有信息。业务操作是否达到目标仍需核对输出和回执。`result`、某条主动交付消息或 Run 的 `idle` 都不能代替该轮的终态。 + +没有匹配的 `clientRequestId` 或没有结构化 `terminal` 时,示例不会把事件判成这次请求已经成功。状态文件保留已观察的信息,供后续对账。 + +### 区分各种 ID {#conversation-identifiers} + +| ID | 标识范围与生命周期 | +|---|---| +| `clientInstanceId` | 客户端接收通道;恢复同一示例状态时沿用 | +| `originSocketId` | 一次 Socket.IO 连接;每次重连变化 | +| `clientRequestId` | 一次 start 或 follow-up 提交;同一提交重发不变,每轮新请求不同 | +| `userMessageId` / `message.id` | 本轮用户消息;start 和 follow-up 使用不同字段位置 | +| `conversationId` | 对话范围,可跨多轮使用 | +| `sessionId` | 服务分配的运行会话;只有服务确认可复用时继续使用 | +| `runId` | 持久运行记录,可跨多轮复用;每轮可能产生独立回执 | +| `instanceId` | 握手中的数据空间身份,用于确认目标实例 | +| `serverInstanceId` | 接纳查询中的服务进程代际,重启会改变;不是数据空间身份或凭据 | + +## 接纳未知、重试与断线对账 {#request-reconciliation} + +发生超时、响应丢失或断线后,先保留原请求的 ID、内容摘要和提交时间。用只读接纳查询确认观察结果,再决定是否允许有限重发;不要为了重试而生成新的请求 ID。 + +### `POST /api/query/client-requests/lookup` + +这是带实例认证的只读查询,不发起模型执行。JSON 顶层及 `requests` 每项不接受未声明字段: + +```json +{ + "agentId": "your-agent-id", + "conversationId": "your-agent-id", + "requests": [ + { + "clientRequestId": "00000000-0000-4000-8000-000000000002", + "userMessageId": "00000000-0000-4000-8000-000000000003" + } + ] +} +``` + +`agentId` 必填,`conversationId` 可选;`requests` 为 1—100 项,每项的 `clientRequestId` 必填、`userMessageId` 可选,ID 使用非空且不超过 256 字符的字符串。成功响应包括 `protocolVersion: 1`、`serverInstanceId`、`observedAt`、`autoReplayRetentionMs`、`tombstoneRetentionMs` 和 `results`。 + +| `results[].status` | 含义与处理 | +|---|---| +| `committed` | 已提交,不重发。可能提供 `committedAt`、`sessionId`、`committedServerInstance`、`executionWitness`;不代表执行完成 | +| `reserved` | 已预留,等待后续明确结果;不并发重发 | +| `absent` | 截至查询时没有可确认记录;只有在服务给出的保护窗内、保留原 ID 与相同请求内容时才考虑有限重发 | +| `unknown` | 无法可靠判断;停止自动重发,保留未知并核实执行情况 | + +`committedServerInstance` 可为 `current`、`previous`、`unknown`,描述提交与服务进程代际的关系;`executionWitness` 可为 `user-message`、`none`、`unknown`,描述已观察的执行证据。不要把 `committed`、代际或证据字段当成业务完成结论。 + +示例按响应中的保护窗和服务端 `observedAt` 计算请求年龄,不硬编码一个永久重试期限。超过保护窗时即使查到 `absent` 也不自动再发。收到 `CLIENT_REQUEST_BODY_MISMATCH` 表示同一请求 ID 的内容摘要不一致,应停止重发并核对保存的原请求;新任务与原请求的恢复分别处理。 + +### 恢复与只读检查 + +恢复已保存的请求: + +```bash +node agent-client.mjs --resume ./request.json +``` + +示例保持原请求 ID 和内容,只更新重连后的 `originSocketId`。已提交请求不重发,已预留请求继续观察,未知或保护窗外请求停止自动重发。恢复不会用一条新 start 替换已经失效的原会话。URL 与握手实例身份必须与保存的目标一致。 + +仅查看接纳与可读取的运行记录: + +```bash +node agent-client.mjs --recover ./request.json +``` + +这个模式不建立 Socket.IO 订阅、不发送 start 或 follow-up。已有 `runId` 时读取运行态与回执;只有 `sessionId` 时查找准确匹配的 Run,不猜“最近一条”就是本次运行。对账数据不能重放离线期间全部事件,也不能自动补发曾经发生的外部操作。缺少该轮终态证据时仍保留结果未知。 + +示例退出码为 `0`(观察到完成轮次)、`1`(拒绝、失败或中止)、`2`(结果未知或只读恢复模式)。`--recover` 完成只读检查后仍返回 `2`,这不是重新执行任务或确认任务失败的信号。 + +### 显式取消 + +```bash +node agent-client.mjs --cancel ./request.json +``` + +尚未记录终态时,取消通过 Socket.IO `query:abort` 发送已知的会话、对话、请求与智能体标识,没有同步 ack。继续等待可关联的 `query:complete` 中止终态;没有确认时仍按未知处理。已记录终态时,示例报告该状态而不再发送取消。`--resume`、`--recover`、`--cancel` 互斥,不能再附 `--prompt` 或 `--state`。按 Ctrl+C 只停止这个示例客户端,不会隐式取消服务中的任务。 + +### 提交与恢复错误 + +| HTTP 状态或标识 | 处理方式 | +|---|---| +| `400`,请求或接纳查询不符合字段要求 | 按对应端点的请求形状修正;lookup 的错误为 `client request lookup body does not match schema` | +| `401` / `403` | 核对实例令牌、页面来源及 HTTP/Socket 主体与目标连接,不通过修改标记绕过认证 | +| `409`,`busy: true` 且 `retryable: true` | 会话正在清空;等待清空结束后沿用原请求 ID 和内容重试,不更换 ID | +| `409 CLIENT_REQUEST_BODY_MISMATCH`,`retryable: false` | 原 ID 与请求内容不一致;停止重发,核对保存的原请求 | +| `503`,连接、请求登记或接纳确认不可用 | 保留实际 `retryable` 与错误信息,先查接纳状态;不假设所有 `503` 都可直接重新提交 | +| 丢失响应、断线或缺少可关联终态 | 先只读对账;不能确认时保留未知并停止自动重发 | + +## 相关参考 + +- [OpenAPI 下载](./07-openapi.md):公开 HTTP 操作、参数与响应 Schema。 +- [错误与排查](./03-errors-and-troubleshooting.md):连接、认证、启动与异步问题。 diff --git a/docs/07-api/05-model-proxy.md b/docs/07-api/05-model-proxy.md new file mode 100644 index 00000000..4424352c --- /dev/null +++ b/docs/07-api/05-model-proxy.md @@ -0,0 +1,206 @@ +--- +title: 模型协议代理 +description: 取得本机模型代理地址与 Provider 绑定的安全令牌,调用 Messages、Chat Completions 和 Models,并处理流式与兼容限制。 +slug: /api/model-proxy +sidebar_position: 5 +keywords: [API, Compat Proxy, 模型代理, 安全令牌, Provider, Messages, Chat Completions] +--- + +# 模型协议代理 + +DesireCore 的 **Compat Proxy** 让本机程序使用已配置的模型供应商,以 Messages 或 Chat Completions 格式发起请求。调用方使用 `desirecore_` 开头的安全令牌,代理负责取得对应供应商凭据并转发或转换请求。 + +本页适用于与代理运行在同一台机器上的受信任程序。默认入口只监听 `127.0.0.1`,端口动态分配;它与 Agent Service 的 `/api` 及实例访问令牌分别使用。代理请求体上限为 `256 MiB`。 + +## 获取地址、模型与安全令牌 {#proxy-credentials} + +1. 在[算力服务](../02-user-guide/10-settings/04-compute-service.md)中配置并验证目标供应商。 +2. 在 DesireCore 对话中,请受信任智能体调用 `InspectModels`,核对目标 **Provider ID** 与准确的 `modelName`。同名模型可能属于不同 Provider,使用具体配置的 ID。 +3. 请智能体调用 `ComputeCredential` 的 `action: "list"`,确认可取得凭据的 Provider,并读取当前本机兼容代理的 Base URL。这个地址已包含 `/v1`。 +4. 对选定 Provider 调用 `ComputeCredential`,参数为 `{"action":"get","provider":""}`,不设置 `raw: true`。默认返回可撤销的 `desirecore_` 安全句柄。 +5. 请智能体将句柄注入你指定的受保护本机程序配置,并用下面的小请求验证。工具卡可能显示敏感结果占位,安全 Token 面板也只展示识别片段;不以它们作为复制完整句柄的入口。 + +可以这样提出任务: + +> 查询我已配置的对话模型,确认 Provider ID 和 modelName。用 ComputeCredential list 取得当前本机兼容代理 Base URL,再为选定 Provider 获取默认安全 token,将它注入我指定的本机程序配置,并验证一次只回复 OK 的请求。 + +如果工具显示代理未运行,先恢复服务再调用;如果已有实例的工具结果没有地址,请管理员提供已确认的当前回环地址。`InspectModels` 的模型元数据不代替代理地址;供应商上游 `baseUrl` 也不能用于发送这个安全句柄。 + +| 示例变量 | 来源与含义 | +|---|---| +| `PROXY_BASE_URL` | 当前代理返回或管理员确认的本机 Base URL,已含 `/v1`,不带末尾斜杠 | +| `PROXY_TOKEN` | `ComputeCredential` 默认 `get` 返回的完整安全句柄 | +| 模型名 | 选定 Provider 中准确的 `modelName`;示例中的占位值需替换 | + +同一个安全令牌只绑定签发时指定的 **Provider 配置 ID**。即使请求传入其它 `x-desirecore-provider-id`,也不能改换它所绑定的 Provider;绑定的 Provider 不服务该模型时不会转到另一家。当前不能把这种绑定理解为模型级白名单、调用预算或自动到期限制。 + +句柄是一份可使用的访问凭据,保存在受保护本机配置中。任何可访问该本机入口且持有句柄的程序都能使用其绑定的 Provider;签发记录中的 Agent 名称不代表每次 HTTP 调用都重新验证了该程序的身份。 + +## 查看模型列表 {#proxy-models} + +### `GET /v1/models` + +使用 Bearer 令牌或 `x-api-key` 认证,无请求体和分页参数: + +```bash +PROXY_BASE_URL='replace-with-the-returned-local-base-url' +PROXY_TOKEN='replace-with-your-desirecore-safe-token' + +curl --silent --show-error \ + --header "Authorization: Bearer ${PROXY_TOKEN}" \ + "${PROXY_BASE_URL}/models" +``` + +```json +{ + "object": "list", + "data": [ + { + "id": "your-model-name", + "object": "model", + "created": 1791943200, + "owned_by": "openai" + } + ] +} +``` + +`id` 为模型名,`owned_by` 为供应商类型,不是 Provider 配置 ID;`created` 是列举时生成的 Unix 时间,不能当作模型发布时间。 + +当前模型列表汇总所有启用 Provider 的配置模型,**没有按安全令牌绑定的 Provider 筛选**。列表出现某个模型不证明这枚令牌可调用它,也不证明该模型当前可用;调用仍受 Provider 绑定、配置与供应商状态约束。用 `InspectModels` 的 Provider 信息核对你的目标。 + +## Messages 请求 {#proxy-messages} + +### `POST /v1/messages` + +使用 Anthropic Messages 请求格式。下面使用文本消息、不启用流式,并将输出上限设为 `64`: + +```bash +curl --silent --show-error --include --request POST \ + --header "x-api-key: ${PROXY_TOKEN}" \ + --header 'anthropic-version: 2023-06-01' \ + --header 'Content-Type: application/json' \ + --data '{"model":"your-model-name","max_tokens":64,"stream":false,"messages":[{"role":"user","content":"Reply only with OK."}]}' \ + "${PROXY_BASE_URL}/messages" +``` + +| 字段 | 用途 | +|---|---| +| `model` | 绑定 Provider 中的准确模型名 | +| `messages` | Messages 消息数组,本例为一条 user 文本消息 | +| `max_tokens` | Messages 格式的输出上限 | +| `system` | 可选系统提示;字符串或符合该协议的系统内容块 | +| `stream` | 可选布尔值,默认 `false`;为 `true` 时接收 SSE | +| `temperature`、`top_p`、`stop_sequences` | 可选生成参数,须符合目标模型的限制 | +| `tools`、`tool_choice`、`thinking`、`output_config` | 可选工具与思考/输出配置,受协议转换及目标后端支持范围约束 | + +非流式响应结构示例: + +```json +{ + "id": "msg_example", + "type": "message", + "role": "assistant", + "content": [{ "type": "text", "text": "OK" }], + "model": "your-model-name", + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { "input_tokens": 6, "output_tokens": 1 } +} +``` + +读取 `content` 和 `stop_reason`。例如 `tool_use` 表示模型请求工具调用,不表示工具已经执行;程序需要自己执行获准工具并按协议提交工具结果。`max_tokens` 等停止原因也不能当成完整业务成功。 + +## Chat Completions 请求 {#proxy-chat-completions} + +### `POST /v1/chat/completions` + +使用 OpenAI Chat Completions 请求格式: + +```bash +curl --silent --show-error --include --request POST \ + --header "Authorization: Bearer ${PROXY_TOKEN}" \ + --header 'Content-Type: application/json' \ + --data '{"model":"your-model-name","max_completion_tokens":64,"stream":false,"messages":[{"role":"user","content":"Reply only with OK."}]}' \ + "${PROXY_BASE_URL}/chat/completions" +``` + +| 字段 | 用途 | +|---|---| +| `model`、`messages` | 目标模型与 Chat Completions 消息数组 | +| `max_completion_tokens` | 可选输出上限,符合目标模型和后端约束 | +| `stream`、`stream_options` | 可选流式设置;支持的用量信息取决于后端 | +| `temperature`、`top_p`、`stop` | 可选生成参数 | +| `tools`、`tool_choice`、`parallel_tool_calls` | 可选 function 工具定义和选择;转换不代表代理替程序执行工具 | +| `reasoning_effort`、`response_format` | 可选思考与输出约束,须符合目标模型和转换后端的支持范围 | + +```json +{ + "id": "chatcmpl_example", + "object": "chat.completion", + "created": 1791943200, + "model": "your-model-name", + "choices": [ + { + "index": 0, + "message": { "role": "assistant", "content": "OK" }, + "finish_reason": "stop" + } + ], + "usage": { "prompt_tokens": 6, "completion_tokens": 1, "total_tokens": 7 } +} +``` + +读取 `choices[].message`、`finish_reason` 和实际返回的用量;`tool_calls`、`length` 等结束原因需要分别处理。用量字段缺席不能当作零消耗。 + +## 流式与协议范围 {#proxy-streaming} + +将请求中的 `stream` 改为 `true`,用支持 SSE 的客户端读取;curl 可添加 `--no-buffer`。Messages 与 Chat Completions 各自返回对应协议的流式数据。代理会按目标供应商的接入格式选择转发或转换,因此它们不是完整供应商 API 的通用替代。 + +HTTP `200` 或第一段文本不能证明流正常结束。客户端需要读取该协议的结束标记,同时检查流内错误;中途断线、超时、空响应或未收到结束信息时,保留已读内容并报告未完整完成。错误可能发生在响应头已经发出之后,不能只检查最初的 HTTP 状态。 + +跨协议工具定义和结构化输出受转换器及目标后端约束。尤其是订阅后端可能拒绝不支持的 Schema、工具选择或参数;不要把给 DesireCore 内部工具生成的标记、beta 参数或复杂 Schema 当成外部接口普遍支持。先使用本页的文本小请求,再按目标后端逐项验证所需能力。 + +本页公开的入站接口为 `/v1/models`、`/v1/messages`、`/v1/chat/completions`。没有 `/v1/responses` 入站接口承诺;配置中使用 Responses 的供应商不意味着代理接受同名请求。 + +## 更新与吊销令牌 {#proxy-token-lifecycle} + +安全句柄没有自动过期时间,可能在同一执行会话内复用,也不会因会话结束或服务重启自动作废。服务重启后端口可能变化,需要重新取得当前代理地址。 + +在算力资源的 **安全 Token** 面板可以吊销相应条目,或将全部未吊销句柄作废;随后需要重新取得句柄并更新调用方。若页面提示当前环境不支持管理,使用管理员支持的桌面管理入口处理。面板不是令牌签发或复制完整句柄的入口。 + +吊销影响后续认证,不保证终止已经转发的请求。Provider 的停用、模型移除、登录状态、上游凭据刷新或额度限制也可能使后续调用失败;它们与安全句柄是否已吊销分别排查。 + +## 错误与使用条件 {#proxy-errors} + +鉴权失败采用下列格式,即使请求的是 Chat Completions: + +```json +{ + "type": "error", + "error": { + "type": "authentication_error", + "message": "Unauthorized compat-proxy request" + } +} +``` + +| 状态或现象 | 处理方式 | +|---|---| +| `401 authentication_error` | 核对本机入口、安全句柄及吊销状态;实例令牌与供应商真实 Key 不能代替这里的句柄 | +| `400 Missing required field: model` | 使用绑定 Provider 中准确的模型名 | +| `400`,绑定 Provider 不服务该模型 | 核对目标 Provider、启用状态及模型配置;不会切换其它 Provider | +| 参数或工具 Schema 被拒绝 | 根据实际错误和后端兼容范围修正,不能用内部请求标记绕过限制 | +| `413` / `415` | 缩小请求体到 `256 MiB` 以内,并使用 `Content-Type: application/json` | +| 上游认证、限流或额度错误 | 检查供应商配置、账户资格与实际错误,遵守返回的重试条件 | +| `502` / `504` 或流内错误 | 检查网络、上游响应和超时;保存未完成状态,不把部分文本当成完整结果 | +| 未注册路径 | 修正到本页已列出的接口;有效凭据不保证未知路径存在 | + +代理接受 `x-api-key` 或 Bearer 认证,同时发送两者时优先使用非空的 `x-api-key`。错误格式也随 Messages、Chat Completions、上游响应与流式阶段不同,按实际正文处理。 + +使用既有供应商账户、订阅或云端算力仍受相应账户、额度和授权条件限制。安全句柄与协议转换不授予新的供应商使用资格;详见[算力服务的授权条件](../02-user-guide/10-settings/04-compute-service.md)和[使用条款](../terms.md)。 + +## 相关参考 + +- [连接实例与认证](./01-connection-and-auth.md):Agent Service 地址与实例凭据。 +- [OpenAPI 下载](./07-openapi.md):代理操作的独立规范与覆盖范围。 diff --git a/docs/07-api/06-workflow-integration.md b/docs/07-api/06-workflow-integration.md new file mode 100644 index 00000000..1d9dde78 --- /dev/null +++ b/docs/07-api/06-workflow-integration.md @@ -0,0 +1,192 @@ +--- +title: 工作流集成 +description: 使用独立 Workflow Service 的地址与访问令牌,触发管理员预置工作流并观察实时事件和执行历史。 +slug: /api/workflow-integration +sidebar_position: 6 +keywords: [API, Workflow Service, 工作流, Webhook, SSE, 执行历史, 授权] +--- + +# 工作流集成 + +可信的部署所有者或管理员客户端可以通过**独立部署的 Workflow Service** 触发已有工作流,再用返回的运行 ID 观察事件和读取历史。本页说明管理员已经部署、验证并允许从 Webhook 入口触发的全局工作流,不提供任意工作流或任意用户资源的执行授权。 + +## 准备独立地址与凭据 {#workflow-connection} + +| 信息 | 获取方式 | +|---|---| +| `WORKFLOW_URL` | 管理员交付的 Workflow Service 地址。直接本机调用使用实际监听地址;远程调用使用管理员另外配置的 HTTPS 地址 | +| `WORKFLOW_SERVICE_TOKEN` | 独立工作流进程启动配置中的 `WORKFLOW_SERVICE_TOKEN`;部署未单独设置时可能采用所配置的 `AGENT_SERVICE_TOKEN`,须由管理员确认 | +| `WORKFLOW_ID` | 管理员提供的已部署全局工作流 ID,可用下文列表核对;显示名称不能替代 ID | +| 参数约定 | 管理员说明工作流允许的输入、执行操作与结果。先用一个无外部副作用的测试流程确认接入 | + +工作流服务默认绑定本机回环地址,实际端口以启动就绪信息或管理员交付为准。它与 Agent Service 使用不同的服务地址和请求头;即使部署复用了同一个令牌值,仍发送 `X-Workflow-Service-Token`。 + +默认 Agent Service 容器部署不包含对外工作流服务入口,其反向代理会拒绝 `/api/workflow/*`。不能只在 Agent Service 的地址后添加工作流路径就认为可用。需要管理员单独提供可访问的 Workflow Service 地址。 + +本页的独立服务接入中,所有下述业务请求,包括 SSE,都必须发送有效的 `X-Workflow-Service-Token`。这是一份服务访问凭据,不是某个用户、任务或工作流的独立执行授权。`runId` 和工作流 ID 也不能代替凭据或资源权限检查。 + +## 核对可触发工作流 {#list-workflows} + +### `GET /api/workflows` + +无请求体,不提供分页参数。返回 `{ "workflows": [...] }`,每项包含: + +| 字段 | 含义 | +|---|---| +| `id`、`name`、`description`、`creator` | 工作流 ID、显示名称、说明和创建者描述 | +| `nodeCount` | 定义中的节点数量 | +| `dslPath` | 服务端管理的定义路径;不是调用方机器上的文件路径,也不是访问凭据 | +| `lastRun` | 可选的最近运行压缩摘要,包含 `status`、`taskBoardBound`,可能有完成时间和耗时 | + +```bash +WORKFLOW_URL='https://workflow.example.com' +WORKFLOW_SERVICE_TOKEN='replace-with-the-configured-workflow-service-token' +WORKFLOW_ID='your-workflow-id' + +curl --silent --show-error \ + --header "X-Workflow-Service-Token: ${WORKFLOW_SERVICE_TOKEN}" \ + "${WORKFLOW_URL}/api/workflows" +``` + +列表发现用于核对管理员交付的定义,不证明所有工作流都获准执行;无法解析的定义也可能不在列表中。 + +## 触发预置工作流 {#trigger-workflow-webhook} + +### `POST /api/workflow/webhook?id=...` + +| 项目 | 约定 | +|---|---| +| 查询参数 `id` | 必需,已部署全局工作流的 ID | +| 请求头 | `X-Workflow-Service-Token`、`Content-Type: application/json` | +| 请求体 | 本例使用 JSON 对象,其字段作为该工作流的参数;省略时按空对象处理 | +| 成功响应 | `200`,包含 `runId`、`workflowId`、`status: "started"` | + +下面假设管理员已为这个工作流约定 `name` 参数。实际字段按工作流的输入要求替换: + +```bash +curl --silent --show-error --include --request POST \ + --header "X-Workflow-Service-Token: ${WORKFLOW_SERVICE_TOKEN}" \ + --header 'Content-Type: application/json' \ + --data '{"name":"API"}' \ + "${WORKFLOW_URL}/api/workflow/webhook?id=${WORKFLOW_ID}" +``` + +```json +{ + "runId": "00000000-0000-4000-8000-000000000010", + "workflowId": "your-workflow-id", + "status": "started" +} +``` + +保存实际返回的 `runId`。`started` 表示服务已接收并安排这次异步运行,后续定义解析、节点执行、权限或服务依赖仍可能失败。它不代表工作流完成。 + +这个工作流 Webhook 使用服务令牌,与[智能体 Webhook](./02-agent-webhook.md)的 Bearer secret 和事件体格式不同。本入口没有请求去重或公开幂等键;把业务事件 ID 放进参数也不会自动去重。响应丢失时可能已经执行,不要直接重发来猜测结果。 + +## 观察实时事件 {#workflow-events} + +### `GET /api/workflow/events?runId=...` + +发送返回的 `runId` 和服务令牌,响应为 `text/event-stream`: + +```bash +WORKFLOW_RUN_ID='replace-with-the-returned-run-id' + +curl --no-buffer --silent --show-error --get \ + --header "X-Workflow-Service-Token: ${WORKFLOW_SERVICE_TOKEN}" \ + --data-urlencode "runId=${WORKFLOW_RUN_ID}" \ + "${WORKFLOW_URL}/api/workflow/events" +``` + +每个事件由 `event:` 名称和 JSON `data:` 组成,常见事件包括: + +| 事件 | 观察内容 | +|---|---| +| `workflow:started` | 工作流与运行 ID、名称、总节点数 | +| `workflow:node_start` | 节点 ID、类型与可能存在的显示名 | +| `workflow:node_complete` | 节点输入/输出与耗时 | +| `workflow:node_error` | 节点 ID 和错误;还需观察整次运行结果 | +| `workflow:human_gate` | 人工确认等待信息,不等于运行完成 | +| `workflow:complete` | 运行状态和可能存在的最终输出;必须读取 `status` | +| `workflow:error` | 运行错误,可能包括失败节点 | + +同一运行使用一个活动订阅。该流按服务访问凭据放行,供可信的管理员客户端使用,不能作为任意终端用户按 Run ID 读取资源的权限接口。浏览器原生 `EventSource` 不能直接设置这个自定义认证头;浏览器集成需使用能带请求头的 fetch 流读取,令牌不放入 URL。 + +已经在当前服务进程结束的运行重新连接时,可能只收到一条 `workflow:complete`,其中 `status` 也可能是 `failed`。事件名称本身不代表成功。历史运行或进程重启后不再存在的实时上下文可能返回 `404`,应改查下面的历史端点。 + +SSE 不是持久的全部事件重放接口。断线、流结束或没有收到事件都不能独立证明任务成功;使用运行 ID 读取状态与历史,对无法确认的情况保留未知。 + +## 读取运行历史 {#workflow-history} + +### `GET /api/workflow/:id/runs` + +返回 `{ "runs": [...] }`,合并当前活跃运行和已保存历史,按 `startedAt` 从新到旧排序,不提供分页参数。摘要项的关键字段示例: + +```json +{ + "runs": [ + { + "runId": "00000000-0000-4000-8000-000000000010", + "status": "completed", + "startedAt": "2026-10-14T02:00:00.000Z", + "nodeCount": 2, + "completedNodeCount": 2, + "taskBoardBound": false + } + ] +} +``` + +摘要还可能含 `completedAt`、`totalDurationMs`、`error` 和已授权的任务/执行归属信息。活跃运行的节点计数可能暂为 `0`,不能据此推断工作流定义没有节点。 + +### `GET /api/workflow/:id/runs/:runId` + +返回单次运行的详情对象,包含 `workflowId`、`runId`、`status`、`startedAt` 和 `taskBoardBound`。结束后可能有 `completedAt`、`totalDurationMs`、`nodeResults`、`finalOutputs`、`error`;等待人工确认时可能有 `humanGate`。 + +```bash +curl --silent --show-error \ + --header "X-Workflow-Service-Token: ${WORKFLOW_SERVICE_TOKEN}" \ + "${WORKFLOW_URL}/api/workflow/${WORKFLOW_ID}/runs/${WORKFLOW_RUN_ID}" +``` + +`running`、`waiting_human` 表示仍在执行或等待确认;`completed`、`failed`、`cancelled`、`interrupted` 是结束状态。核对 `status`、错误和输出后判断本次运行。Workflow 的 `runId` 属于工作流服务,不能拿去调用 Agent Service 的 `/api/runs/:id/receipt`。 + +### TaskBoard 关联记录 + +历史列表与详情会检查关联的 TaskBoard 资源权限;无法读取的任务记录不会作为可见结果返回。按任务范围查询时,参数为: + +| 查询参数 | 条件 | +|---|---| +| `boardId` | 指定任务范围时必需 | +| `workflowBindingId` | 指定任务范围时必需,必须属于该工作流及相应 Board/Task | +| `taskId` | 可选;提供时必须非空且与相应绑定匹配 | + +这些是筛选与资源定位信息,不授予访问权。字段组合无效或资源权限不足时可能返回 `403`;详情不在指定任务范围内时返回 `404`。列表为空也可能来自权限筛选或不可读历史,不能据此推断从未执行。 + +## 手动运行与定义修改的支持范围 {#workflow-supported-scope} + +`POST /api/workflow/run` 要求有效 `executionGrantId`、`idempotencyKey` 和对应定义引用或获准的定义路径;服务访问令牌不能替代执行授权。上述触发与读取接口不签发这项授权。手动运行需要产品支持的工作流操作,或管理员另外提供的授权集成流程;本页的快速开始使用预置 Webhook。不能自造 grant 或复制内部权限对象。 + +旧 `POST /api/workflows` 创建定义和 `PUT /api/workflow/:id` 覆盖定义的入口已退役,返回 `410`。使用现有工作流管理流程维护定义,本页只触发管理员已预置且适用该 Webhook 的工作流。需要人工确认时使用相应产品流程;不能把收到 `human_gate` 消息当成已批准执行。 + +## 错误与恢复 {#workflow-errors} + +| HTTP 状态或现象 | 含义与处理 | +|---|---| +| `401 unauthorized` | 缺少或无效的工作流服务令牌;确认专用请求头与服务地址 | +| `503 workflow_service_not_initialized` | 工作流服务访问凭据未初始化;请管理员检查启动配置 | +| `503 WORKFLOW_OWNER_FENCED` | 服务暂时不能安全提供相关操作;保留响应并请管理员检查服务状态,不无条件重发触发请求 | +| 触发 `400 id query parameter is required` | 补充管理员提供的工作流 ID | +| 触发 `404` | 指定的全局工作流不可读取或不存在;核对服务与 ID | +| 触发 `503 Unable to reserve workflow run` | 无法为这次触发预留运行;确认实际响应并检查服务状态 | +| 事件 `400 runId is required` / `404 Run not found` | 缺少运行 ID,或当前进程没有该实时运行;核对 ID,并读取历史 | +| 历史 `403` / `404` | 资源权限、工作流与运行匹配或任务范围不满足;不要绕过归属检查 | +| 触发成功后失败或 SSE 中断 | HTTP 接收成功不代表完成;按实际返回的运行 ID 查历史,不能确认时保留未知 | +| 请求格式、体积或服务依赖错误 | 保留实际状态与正文,修正请求或请管理员排查依赖;不同错误不保证统一包装 | + +重试只读查询可以更新观察结果;重新触发会创建另一次运行。没有收到触发响应时,不要把某条“最近运行”直接归为本次请求。 + +## 相关参考 + +- [任务编排](../04-concepts/10-task-orchestration.md):工作流的使用方式与节点。 +- [OpenAPI 下载](./07-openapi.md):本页公开操作的参数和响应范围。 diff --git a/docs/07-api/07-openapi.md b/docs/07-api/07-openapi.md new file mode 100644 index 00000000..29bd28f0 --- /dev/null +++ b/docs/07-api/07-openapi.md @@ -0,0 +1,88 @@ +--- +title: OpenAPI 下载 +description: 下载 DesireCore 已公开 HTTP 端点的 OpenAPI 文件,选择正确的实例地址与凭据。 +slug: /api/openapi +sidebar_position: 7 +--- + +# OpenAPI 下载 + +你可以把下面的 JSON 文件导入支持 [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0.html) 的 API 工具,查看参数、响应和认证要求。请求与响应声明使用 JSON Schema Draft 7 方言;文件只描述表中列出的公开 HTTP 端点。 + +**[下载 Agent Service OpenAPI JSON](/openapi/agent-service.json)** + +另外提供 **[模型协议代理 OpenAPI JSON](/openapi/compat-proxy.json)** 和 **[Workflow Service OpenAPI JSON](/openapi/workflow-service.json)**。三份文件各自使用对应服务的地址与凭据。 + +## Agent Service 覆盖范围 + +| 方法与路径 | 用途 | 使用说明 | +|---|---|---| +| `GET /api/client-handshake` | 验证已有实例凭据与服务身份 | [连接实例与认证](./01-connection-and-auth.md) | +| `POST /api/webhook/{agent_id}` | 触发管理员已配置的智能体事件 | [智能体 Webhook](./02-agent-webhook.md) | +| `GET /api/agents/{id}/webhooks` | 读取事件配置与可选工具 | [智能体 Webhook](./02-agent-webhook.md) | +| `PUT /api/agents/{id}/webhooks/events/{event}` | 创建或替换事件,选择所需工具 | [智能体 Webhook](./02-agent-webhook.md) | +| `PATCH /api/agents/{id}/webhooks` | 开启或暂停事件触发 | [智能体 Webhook](./02-agent-webhook.md) | +| `DELETE /api/agents/{id}/webhooks/events/{event}` | 删除已有事件 | [智能体 Webhook](./02-agent-webhook.md) | +| `POST /api/agents/{id}/webhooks/secret/rotate` | 轮换智能体的专用 Webhook 密钥 | [智能体 Webhook](./02-agent-webhook.md) | +| `GET /api/agents/{agentId}/schedule-messages` | 读取可见的后台消息与交付 | [智能体 Webhook](./02-agent-webhook.md) | +| `GET /api/agents` | 读取智能体摘要列表 | [智能体列表](./04-conversations-and-runs.md#list-agents) | +| `GET /api/runs` | 按条件读取实例运行记录 | [运行列表](./04-conversations-and-runs.md#list-runs) | +| `GET /api/runs/{id}` | 读取单个运行的元信息 | [运行详情](./04-conversations-and-runs.md#get-run) | +| `GET /api/runs/{id}/runtime-state` | 读取运行的原始状态 | [运行状态](./04-conversations-and-runs.md#get-run-runtime-state) | +| `GET /api/runs/{id}/receipt` | 读取可信用户有权查看的回执 | [运行回执](./04-conversations-and-runs.md#list-run-receipts) | +| `POST /api/query/start` | 提交一次新对话请求 | [对话与运行](./04-conversations-and-runs.md) | +| `POST /api/query/follow-up` | 向已建立的会话提交下一轮 | [对话与运行](./04-conversations-and-runs.md) | +| `POST /api/query/client-requests/lookup` | 按稳定请求 ID 对账接纳状态 | [对话与运行](./04-conversations-and-runs.md) | + +花括号表示需要替换的路径参数,对应端点指南中的冒号写法。Webhook 管理操作要求认证握手中的 `capabilities.webhookManagement` 为 `true`,并使用实例所有者凭据。旧实例缺少该能力时,使用管理员已启用并验证的事件。运行列表和详情按部署所有者的实例读取范围使用;回执读取另外检查可信用户归属。 + +## 模型协议代理覆盖范围 + +| 方法与路径 | 用途 | +|---|---| +| `GET /v1/models` | 列出启用供应商配置中的模型 | +| `POST /v1/messages` | 发送 Messages 请求,可选择 SSE 流式响应 | +| `POST /v1/chat/completions` | 发送 Chat Completions 请求,可选择 SSE 流式响应 | + +按[模型协议代理](./05-model-proxy.md)取得当前本机 Base URL 和绑定 Provider 的安全句柄。该 Base URL 已包含 `/v1`;在导入工具中设置服务器时使用它的来源地址,保留实际端口,让文件中的路径补上 `/v1`。`ProxyBearer` 使用 Bearer,`ProxyApiKey` 使用 `x-api-key`;同一操作可任选一种。 + +模型列表没有按句柄绑定的 Provider 筛选,列表出现模型不代表该句柄可调用它。文件呈现已公开的协议子集;可选参数、工具与结构化输出仍受转换器及目标后端约束。流内错误需要由客户端读取和处理。 + +## Workflow Service 覆盖范围 + +| 方法与路径 | 用途 | +|---|---| +| `GET /api/workflows` | 读取已部署工作流元数据 | +| `POST /api/workflow/webhook` | 通过必需查询参数 `id` 触发预置工作流 | +| `GET /api/workflow/events` | 通过必需查询参数 `runId` 观察 SSE 事件 | +| `GET /api/workflow/{id}/runs` | 读取可见的运行摘要 | +| `GET /api/workflow/{id}/runs/{runId}` | 读取单次运行详情 | + +使用管理员独立交付的 Workflow Service 地址和 `WorkflowToken` 方案,对应请求头为 `X-Workflow-Service-Token`。默认 Agent Service 反向代理不开放这些工作流路径。见[工作流集成](./06-workflow-integration.md)。 + +工作流文件的响应声明呈现当前实现的字段,允许额外字段,不代表接口对所有字段执行了闭合 Schema 校验。预置 Webhook、事件观察和历史读取不签发 `executionGrantId`;文件不包含要求额外执行授权的 `/api/workflow/run`,也不包含已退役的定义写入操作。 + +## 导入后设置地址与认证 + +将文件的示例服务器 `https://your-instance.example` 替换为管理员交付的实例 HTTPS 地址;如果地址带有非标准端口,保留端口。这个地址不包含 `/api`,也不是文档站点地址。详见[连接实例与认证](./01-connection-and-auth.md)。 + +| 文件中的认证方案 | 提供什么 | 发送方式 | +|---|---|---| +| `InstanceToken` | 已有实例访问令牌 | `X-Agent-Service-Token: <实例令牌>` | +| `WebhookSecret` | 目标智能体的专用 Webhook 密钥 | `Authorization: Bearer ` | + +上表适用于 Agent Service 文件。根据具体操作选择对应的方案。供应商 API Key 用于 DesireCore 连接模型供应商;模型代理和 Workflow 文件使用各自的地址与认证方案。 + +## 使用文件时注意响应含义 + +握手返回 `200` 表示当前凭据通过验证;某项功能能否使用还取决于实例配置、功能权限和实际能力。 + +Webhook 的 `200` 和 `accepted: true` 表示接收了触发,响应没有 Run ID 或最终结果。后台失败不会修改已返回的 HTTP 响应。结果交付、超时及重复请求的处理按 [Webhook 指南](./02-agent-webhook.md)执行。 + +各端点的错误结构不同,生成的调用代码也应保留 HTTP 状态与实际正文。反向代理还可能返回文本或网页;参见[错误与排查](./03-errors-and-troubleshooting.md)。 + +Socket.IO 的连接、订阅与异步事件需要配套客户端逻辑。OpenAPI 文件描述 HTTP 部分,不把这些事件作为 SSE 端点。 + +对话提交的 `202` 也不是运行完成反馈。先连接 Socket.IO,再使用当前连接的 `socket.id` 作为 `originSocketId` 提交;HTTP 请求与 Socket 连接必须属于同一认证主体。稳定的 `clientInstanceId` 位于 Socket 认证数据中。断线后按[对话与运行](./04-conversations-and-runs.md)的对账步骤处理,不把 Run 的 `idle` 状态当作某次提交成功的证据。 + +文件的 `info.version` 是这份 HTTP 接口描述的版本。它不代表 DesireCore 客户端的生效版本,也不代替握手中的 `serverVersion` 或 `apiProtocolVersion`。 diff --git a/docs/07-api/_category_.json b/docs/07-api/_category_.json new file mode 100644 index 00000000..8efb89a1 --- /dev/null +++ b/docs/07-api/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "集成与 API", + "position": 7, + "link": { + "type": "doc", + "id": "api/index" + } +} diff --git a/docs/07-api/index.md b/docs/07-api/index.md new file mode 100644 index 00000000..f115dc19 --- /dev/null +++ b/docs/07-api/index.md @@ -0,0 +1,39 @@ +--- +title: 集成与 API +description: 连接实例、执行对话、配置 Agent Webhook,并接入模型代理和工作流服务。 +slug: /api +sidebar_position: 0 +keywords: [API, 集成, Webhook, Agent Service, 接入, 认证] +--- + +# 集成与 API + +你可以从自己的脚本或服务连接 DesireCore,验证连接、执行对话、从外部事件触发智能体,或接入模型代理和工作流。先取得对应服务的地址与凭据,再根据用途选择指南。 + +## 选择接入方式 + +| 你想做什么 | 所需条件 | 阅读指南 | +|---|---|---| +| 用自己的程序连接实例 | 管理员提供 HTTPS 地址和实例访问令牌 | [连接与认证](./01-connection-and-auth.md) | +| 提交对话并观察结果 | 实例所有者访问权限、Agent ID;运行示例需要 Node.js 22+ | [对话与 Run](./04-conversations-and-runs.md) | +| 从外部事件触发智能体 | 已启用事件和 Agent 级 Webhook secret;所有者配置需服务支持 `webhookManagement` | [Agent Webhook](./02-agent-webhook.md) | +| 通过本机代理调用模型协议 | 正在运行的本机代理和绑定供应商的安全 token | [模型代理](./05-model-proxy.md) | +| 触发现有工作流 | 独立 Workflow Service 地址、服务令牌和管理员提供的定义 | [工作流集成](./06-workflow-integration.md) | +| 将 HTTP 合同导入工具 | 选择对应服务的规范文件 | [OpenAPI 下载](./07-openapi.md) | +| 让 DesireCore 调用你的业务服务 | 通过 MCP、工具或技能连接 | [MCP 服务器连接](../02-user-guide/09-capabilities/03-mcp-integration.md)与[集成能力概览](../02-user-guide/09-capabilities/06-integrations-overview.md) | + +## 凭据与结果 + +Agent Service 使用 `X-Agent-Service-Token`,外部 Webhook 使用 `Authorization: Bearer `。模型代理和 Workflow Service 各有独立凭据与权限范围;模型供应商 API Key 不能代替实例访问令牌。 + +握手的 `200` 证明该次请求已通过认证。对话提交的 `202` 和 Webhook 的 `200 accepted` 表示接纳或接收,应按对应指南观察完成与交付。对话事件使用 Socket.IO,工作流进度使用 SSE。 + +## 开始接入 + +1. 向管理员取得实际服务地址与对应凭据。远程调用使用经过证书验证的 HTTPS 地址。 +2. 完成连接验证或所选指南中的最小示例。 +3. 检查实际响应与结果交付。遇到失败时参见[错误与排查](./03-errors-and-troubleshooting.md)。 + +:::tip 文档地址与实例地址 +`https://docs.desirecore.com/api` 是文档页面。调用接口时,使用你自己的实例地址,例如 `https://agent.example.com`。 +::: diff --git a/docs/index.md b/docs/index.md index 27564da7..8b9269e7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -78,6 +78,7 @@ keywords: [DesireCore, 用户手册, AI 智能体, 委派式交互, Agent OS] ## 热门话题 +- [集成与 API](./07-api/index.md) — 外部程序接入实例与已配置的 Webhook - [配置 API Key](./getting-started/configure-api-key) — 连接 AI 服务的第一步 - [教会智能体](./user-guide/teaching/delegation-interaction) — DesireCore 最核心的能力 - [智能任务编排](./concepts/task-orchestration) — 自动拆解任务、匹配最佳智能体 diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 1430429e..b31bbb44 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -130,6 +130,11 @@ const config: Config = { label: '更多', position: 'left', }, + { + to: '/api', + label: 'API', + position: 'left', + }, // ===== 右侧 ===== { type: 'search', @@ -173,6 +178,7 @@ const config: Config = { { label: '概念解析', to: '/concepts' }, { label: '更多', to: '/more' }, { label: '常见问题', to: '/faq' }, + { label: '集成与 API', to: '/api' }, ], }, { diff --git a/i18n/en/docusaurus-plugin-content-docs/current.json b/i18n/en/docusaurus-plugin-content-docs/current.json index 69cc63f1..f3827dc4 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current.json +++ b/i18n/en/docusaurus-plugin-content-docs/current.json @@ -130,5 +130,9 @@ "sidebar.more.category.更新日志": { "message": "Changelog", "description": "The label for category '更新日志' in sidebar 'more'" + }, + "sidebar.api.category.集成与 API": { + "message": "Integrations and API", + "description": "The label for category '集成与 API' in sidebar 'api'" } } diff --git a/i18n/en/docusaurus-plugin-content-docs/current/02-user-guide/09-capabilities/06-integrations-overview.md b/i18n/en/docusaurus-plugin-content-docs/current/02-user-guide/09-capabilities/06-integrations-overview.md index 86a60644..474a18d5 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/02-user-guide/09-capabilities/06-integrations-overview.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/02-user-guide/09-capabilities/06-integrations-overview.md @@ -25,6 +25,7 @@ Installed apps and plugins share the Apps entry, with peer tabs for use and conf | **Email** | Manage Gmail, Outlook, and IMAP accounts | [Email Management](../email/overview) | | **Workflows** | Compose triggers, code, LLM, Agent, and human-confirmation nodes | [Task Orchestration](../../concepts/task-orchestration) | | **Apps, Plugins, and Services** | Distinguish app use, plugin contribution management, and service connection diagnostics | [Apps, Plugins, and Services](./app-service-catalog) | +| **Instance API and Webhooks** | Verify instance access from an external program or send a trigger to a configured event | [Integrations and API](../../07-api/index.md) | ## Email Integration @@ -55,6 +56,8 @@ DesireCore records supported tool calls, approvals, and external operations so u ## Custom Integrations +To connect your program to a DesireCore instance or trigger a configured Agent Webhook from an external event, see [Integrations and API](../../07-api/index.md). A successful Webhook response only confirms receipt of the trigger; its guide explains how results can be delivered. + Choose the extension path that matches the task: 1. **Build an MCP service** for a stable internal or external API. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/05-more/index.md b/i18n/en/docusaurus-plugin-content-docs/current/05-more/index.md index 404fc8ad..cb6b86a5 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/05-more/index.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/05-more/index.md @@ -21,3 +21,4 @@ Here you'll find quick reference materials and changelog for DesireCore. | [Open Source Repositories](./09-open-source-repos.md) | Overview of official GitHub repositories | | [Third-Party Software & Licenses](./10-third-party-software.md) | Bundled third-party components and open-source licenses | | [Changelog](./11-changelog/index.md) | Version update history | +| [Integrations and API](../07-api/index.md) | Connect external programs, verify instance access, and call configured Webhooks | diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/01-connection-and-auth.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/01-connection-and-auth.md new file mode 100644 index 00000000..4d760852 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/01-connection-and-auth.md @@ -0,0 +1,154 @@ +--- +title: Connect to an Instance and Authenticate +description: Verify a standalone Agent Service connection using its deployment address and instance token, and distinguish instance authentication from a Webhook secret. +slug: /api/connection-and-auth +sidebar_position: 1 +keywords: [API, standalone, Agent Service, instance token, authentication, client-handshake] +--- + +# Connect to an Instance and Authenticate + +Deployment owners can access their own DesireCore instance from a program or command line. This page uses an already running **standalone Agent Service** to explain where to obtain its address and credentials and perform a read-only connection check. + +## Obtain the Address and Token + +You need two pieces of deployment information: + +| Information | Purpose | Where to obtain it | +|---|---|---| +| Public instance address | `BASE_URL` in the examples | Use the HTTPS address delivered for this instance, including its nonstandard port if applicable | +| Instance access token | The `X-Agent-Service-Token` header | Use the `AGENT_SERVICE_TOKEN` configured when starting the instance | + +If the instance provisioning tool saved a deployment manifest, its `token` field is in `.deploy/instances//instance.json` in the deployment project. Use the address in that instance's delivery information. For a manually started instance, obtain `AGENT_SERVICE_TOKEN` from the process or container startup configuration you maintain. Use the existing deployment token; generating a new value does not authenticate requests to the running instance. + +For example, use `https://agent.example.com` as `BASE_URL` if that is the public address. Preserve any port included in the delivered address. `BASE_URL` contains no `/api`, page path, query parameters, or trailing slash. Container service ports are different from the public delivery address and do not determine its external port. + +`https://docs.desirecore.com/api` is the documentation address. Send the requests below to your own instance. + +## Choose the Appropriate Credential + +| What you want to do | Credential | How to send it | +|---|---|---| +| Call instance APIs as the deployment owner | Instance access token | `X-Agent-Service-Token: ` | +| Trigger an Agent Webhook configured and verified by an administrator | The target Agent's Webhook secret supplied by the administrator | `Authorization: Bearer ` | +| Connect DesireCore to a model provider | Provider API key | Configure it in [Configure API Key](../01-getting-started/04-configure-api-key.md) | + +The instance access token represents the instance owner's access and should be held by the deployment owner or a trusted service. A Webhook secret is used for the corresponding Agent's trigger endpoint; a service that only needs to trigger events can use that secret. Each endpoint still has its own configuration, execution permissions, and approval requirements. + +Desktop instance addresses and credentials are managed by the desktop connection mechanism. The deployment manifest path above does not apply to desktop clients. Credentials obtained through desktop pairing remain valid until revoked; restarting does not revoke them. The `interactive` label does not imply strictly minimal permissions. Pairing credentials are not interchangeable with the owner token, so the owner connection procedure below does not directly apply to them. + +## Verify the Connection + +Replace the placeholders with your existing instance address and token, then run: + +```bash +BASE_URL='https://agent.example.com' +AGENT_SERVICE_TOKEN='replace-with-your-existing-instance-token' + +curl --silent --show-error --include \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/client-handshake" +``` + +### `GET /api/client-handshake` + +| Item | Description | +|---|---| +| Request body | None | +| Required header | `X-Agent-Service-Token` | +| Success status | `200 OK` with a JSON response | +| Response caching | `Cache-Control: no-store, private` | + +The following illustrates the response structure. Use your instance's actual version, identifier, time, and capability values: + +```json +{ + "service": "desirecore-agent-service", + "apiProtocolVersion": "1", + "serverVersion": "10.0.180", + "instanceId": "inst_0123456789abcdefghijklmnopqrstuv", + "deploymentMode": "external", + "clientCompatibility": { + "minimumVersion": "0.0.0", + "maximumVersion": null + }, + "capabilities": { + "workflow": false, + "mediaSignedUrl": true, + "pdfRendering": false, + "localPairing": true, + "queryRequestIdEcho": true + }, + "authentication": { + "kind": "instance-token", + "scopes": ["owner"] + }, + "serverTime": "2026-10-13T08:00:00.000Z" +} +``` + +Check these fields before calling a specific feature: + +| Field | How to use it | +|---|---| +| `service` | Must be `desirecore-agent-service`; reject a different service or a web page | +| `instanceId` | Identifies the instance's data space; saving it helps detect a connection to a different instance | +| `deploymentMode` | `external` and `owner` are standalone modes; `embedded` indicates a desktop instance | +| `apiProtocolVersion` | Handshake and connection protocol identifier, currently the string `"1"` | +| `serverVersion` | Instance software version, useful for diagnosing compatibility issues | +| `clientCompatibility` | Connection compatibility range for DesireCore clients; `maximumVersion: null` means no upper bound is set | +| `capabilities` | Capabilities of the current deployment; a successful connection alone does not make an unavailable feature usable | +| `authentication` | Authentication type and instance scope for the deployment; specific operations still require their corresponding permissions | +| `serverTime` | Response time on the server, useful for diagnosing clock differences | + +The handshake verifies existing credentials and the target service. It does not issue a new token or confirm that model services, Agent permissions, or all optional features have been configured. Interpret client connection compatibility and individual feature effective versions separately. + +### Responses During Startup + +An authenticated handshake can return the following while the instance is starting: + +```http +HTTP/1.1 503 Service Unavailable +Retry-After: 1 +Cache-Control: no-store +``` + +```json +{ + "status": "starting", + "service": "agent-service", + "code": "service_not_ready" +} +``` + +Wait as directed by `Retry-After`, then retry this read-only handshake. If the response persists, inspect the instance startup logs. Missing or invalid tokens still return `401`; waiting repeatedly does not resolve an authentication failure. + +## Health Checks and Connection Verification + +`GET /health` is for probes connecting directly to the service listener. It does not require an instance access token and only reports service status. The default standalone reverse proxy does not expose this path as a public API. Use the authenticated handshake above for external calls. + +A direct probe also receives `503 service_not_ready` during startup. Once the service can accept requests, its response looks like this: + +```json +{ + "status": "ok", + "service": "agent-service", + "protocol": "http", + "fullReady": true +} +``` + +`protocol` identifies the HTTP or HTTPS listener being probed. `fullReady` is a background initialization diagnostic and can remain `false` while the service is already accepting requests. A successful health check does not verify the instance token, target instance identity, or an Agent's execution result. A `200` from `/health` on the public address may also contain a web page, so it is insufficient to establish an API connection. + +## Store and Update Tokens + +Keep the instance token in trusted service configuration. Avoid placing it in page source, public repositories, URLs, or shared logs. Standalone startup logs state that a token is configured without printing its value. + +To update the instance token, change `AGENT_SERVICE_TOKEN` in the startup configuration you maintain, restart or recreate the corresponding service process, and update callers. Restarting with unchanged configuration does not revoke the old token. If you use a deployment manifest, update its token as well to prevent a later startup from restoring the old value. An administrator updates the Webhook secret separately in the target Agent's Webhook configuration and informs callers of the replacement. + +Browser calls also depend on HTTPS, browser cross-origin rules, and any allowed origins configured by the deployment owner. When origin restrictions are enabled, `AGENT_SERVICE_CORS_ORIGIN` contains the calling page's exact origin, such as `https://app.example.com`, without a page path. An allowed origin still needs valid credentials. Do not embed an owner token in a page delivered to other users. + +## Next Steps + +- [Agent Webhook](./02-agent-webhook.md): trigger events with the target Agent's dedicated secret. +- [Errors and Troubleshooting](./03-errors-and-troubleshooting.md): resolve authentication, startup, request, and asynchronous execution issues. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/02-agent-webhook.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/02-agent-webhook.md new file mode 100644 index 00000000..70ea8aa8 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/02-agent-webhook.md @@ -0,0 +1,268 @@ +--- +title: Agent Webhook +description: Create Agent Webhook events, select execution permissions, rotate secrets, trigger events, and observe explicit user deliveries. +slug: /api/agent-webhook +--- + +# Agent Webhook + +An external system can send a configured event to DesireCore so the target Agent processes its content in the background. This is useful for business notifications, monitoring events, and messages from other applications. + +Instance owners can create events, select tools, and obtain a dedicated secret through the management API. External services that only send events obtain the configured event details and secret from the administrator. + +First use the [authenticated handshake](./01-connection-and-auth.md) and check `capabilities.webhookManagement`. Only an instance reporting `true` supports the management steps below. If the field is missing or `false`, this instance cannot create or change events through these APIs. Existing valid configurations can still use the [trigger request](#request). + +## Before calling + +| Information | Source and requirements | +| --- | --- | +| `BASE_URL` | The Agent Service address supplied by the administrator; see [Connection and authentication](./01-connection-and-auth.md). The public documentation website is not a service address. | +| `AGENT_ID` | The target Agent ID supplied by the administrator. A display name cannot replace it. Webhooks must be enabled for this Agent. | +| `event` | An existing event name supplied by the administrator, such as `demo.ping`. It must match exactly. | +| `WEBHOOK_SECRET` | The target Agent's dedicated Webhook secret. Store and inject it through the sender's secret management system. Coordinate rotation with the administrator and update the sender. | +| Event behavior | Confirm the expected fields, permitted operations, and result delivery. Trigger requests do not contain tool or file permission settings; actual operations depend on the selected tools and file scope. | + +External callers need a configured Webhook secret. A remote instance rejects triggers without one; a local instance without one also cannot be triggered through the external calling method described here. This secret has a different purpose from an instance access token or a model provider's API key. Configure the sender with the dedicated Webhook secret supplied by the administrator. + +The sender must be able to reach the instance address. A desktop instance's loopback address is only reachable from the same computer; an external server needs a reachable service address supplied by the administrator. The DesireCore service must remain running. + +## Create an event and select execution permissions + +The standalone instance owner performs these steps using `X-Agent-Service-Token` from [Connection and authentication](./01-connection-and-auth.md). A Webhook secret grants triggering access, not event management or receipt access. The commands require `curl` and `jq`. + +### 1. Read events and available tools + +Use the existing instance address, owner token, and target Agent ID. Obtain the Agent ID from the [Agent list](./04-conversations-and-runs.md). The saved management response reports whether a secret exists without returning its value. + +```bash +BASE_URL='https://your-instance.example' +AGENT_ID='your-agent-id' +AGENT_SERVICE_TOKEN='replace-with-your-existing-instance-token' +umask 077 + +curl --fail-with-body --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/agents/${AGENT_ID}/webhooks" \ + --output webhook-config.json + +jq '{enabled,hasSecret,events,availableTools}' webhook-config.json +``` + +| Response field | Meaning | +| --- | --- | +| `enabled` | Whether the Agent's Webhook endpoint is enabled. | +| `hasSecret` | Whether a secret is configured. Reading does not return its value again. | +| `events` | Each existing event's `event`, `promptTemplate`, `maxTurns`, and `toolIds`. | +| `availableTools` | The `id`, `name`, `riskLevel`, and `requiresConfirmation` of tools currently available for selection. | + +Choose the IDs of tools the event needs. `toolIds: []` authorizes no tools. This example selects `SendUserMessage` to deliver the test result to the user's conversation. If it is absent from `availableTools`, adjust the target Agent's tool configuration and read the list again. + +Select only tools needed for the event's work. This example needs only `SendUserMessage`. Add tools such as `Write` or `Bash` when the event needs file writing or command execution. These tools can modify configuration files within their effective file scope, including Agent and event configuration in that scope. Tool selection and file scope use the instance's existing permission mechanism; granting write access also requires considering which configurations it can change. + +### 2. Save `demo.ping` + +The management API accepts the event template, selected tool IDs, and maximum turn count. The service determines the corresponding tools and current file access scope when saving. You do not need to supply tool definitions or permission objects. Execution remains subject to the Agent's current permissions. + +| Field | Requirements | +| --- | --- | +| Event name (`:event` in the path) | 1–128 characters. Start with an ASCII letter or digit; remaining characters may be ASCII letters, digits, `.`, `_`, or `-`. `constructor`, `prototype`, and `__proto__` are not allowed. | +| `promptTemplate` | Required nonempty string, at most 32,768 characters. Supports the `{{key}}` fields described below. | +| `toolIds` | Required array of IDs from `availableTools`, without duplicates. Maximum 512 items; an empty array grants no tool permissions. | +| `maxTurns` | Optional integer from 1 to 100; defaults to 5. | + +`PUT` creates or replaces the named event and enables the Agent's Webhook endpoint. Replacement updates both the template and tool selection; saving again captures the current file access scope. The request body limit is 128 KiB. + +If saving returns `409` with `error: "webhook_configuration_changed"`, the Agent configuration changed during the save, and this event change was not committed. Read events and available tools again with `GET`, confirm the template, tool selection, and current file permissions, then save again. + +```bash +SEND_USER_MESSAGE_TOOL_ID="$(jq -er \ + '[.availableTools[] | select(.name == "SendUserMessage")] | if length == 1 then .[0].id else error("SendUserMessage is not available") end' \ + webhook-config.json)" + +jq -n --arg tool "${SEND_USER_MESSAGE_TOOL_ID}" \ + --arg prompt 'Received message: {{message}}. Use SendUserMessage to send this received message to the user.' \ + '{promptTemplate:$prompt,toolIds:[$tool],maxTurns:5}' \ + > webhook-event.json + +curl --fail-with-body --silent --show-error --request PUT \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --header 'Content-Type: application/json' \ + --data-binary @webhook-event.json \ + "${BASE_URL}/api/agents/${AGENT_ID}/webhooks/events/demo.ping" \ + --output webhook-write.json +``` + +A successful save returns `200`: + +```json +{ + "success": true, + "enabled": true, + "event": "demo.ping", + "secret": "" +} +``` + +`secret` appears **only when this save generated the initial secret**. Immediately store it through the sender's secret management system. Keep response files containing secrets out of public repositories and logs. When a secret already exists, the response contains only `success`, `enabled`, and `event`. + +If the saved secret or its initial response is lost, [rotate the secret](#rotate-the-secret) to obtain a new value. Reading configuration or saving the event again does not retrieve the old secret. + +### 3. Trigger and observe delivery + +Set `WEBHOOK_SECRET` to the generated or rotated value, use [Send one request](#send-one-request) below to trigger `demo.ping`, and follow [Observe the result](#observe-the-result) to verify user delivery. The Agent needs a working model configuration and must actually call its authorized message tool. Saving the event and receiving the trigger do not replace this verification. + +## Pause, delete, and rotate secrets + +All management operations below use the instance access token. They change subsequent trigger configuration without stopping background runs that have already been accepted. + +| Operation | Request | Successful response | +| --- | --- | --- | +| Pause all events | `PATCH /api/agents/:id/webhooks` with JSON `{"enabled":false}` | `{"success":true,"enabled":false}` | +| Enable existing events | Same path with JSON `{"enabled":true}` | `{"success":true,"enabled":true}`; requires existing events and a secret. | +| Delete one event | `DELETE /api/agents/:id/webhooks/events/:event`, no body | `{"success":true,"enabled":...}`; deleting the last event pauses the endpoint. | + +### Rotate the secret + +All events for one Agent share a Webhook secret. After rotation, the old secret can no longer authenticate subsequent triggers, and all senders must update it. Existing runs continue processing. + +```bash +curl --fail-with-body --silent --show-error --request POST \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/agents/${AGENT_ID}/webhooks/secret/rotate" \ + --output webhook-secret.json +``` + +Send no body. Success returns `200 {"secret":""}`. Save the new value in the sender's secret management configuration. If the rotation response is lost, configuration reads cannot retrieve the secret; rotate again and update all senders. + +## Request + +```http +POST /api/webhook/:agent_id +Authorization: Bearer +Content-Type: application/json +``` + +Use the `Bearer ` prefix shown above, immediately followed by the secret. The request body limit is **1 MiB (1,048,576 bytes)** for the entire JSON request. + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `event` | string | Yes | An event name already configured for the target Agent. | +| `payload` | object | No | Fields required by the event template. Omission is treated as an empty object. | + +Only `event` and `payload` are accepted at the top level. `payload` may contain the JSON fields agreed for the event, but must be an object rather than an array or string. + +```json +{ + "event": "demo.ping", + "payload": { + "message": "Test event: please confirm receipt." + } +} +``` + +Transform a third party's original notification into this structure before sending it. For example, a GitHub push integration must map the event name and payload and add the Bearer secret required here. This endpoint does not directly validate GitHub signature headers. + +### Template fields + +The administrator's prompt template uses `{{key}}` to read a matching top-level field from `payload`. For example, `Received message: {{message}}` becomes `Received message: Test event: please confirm receipt.` for the request above. + +- Strings, numbers, and booleans become text. Objects and arrays become JSON text. +- Missing fields and `null` values leave the original placeholder, such as `{{message}}`, unchanged. +- Nested paths such as `{{user.name}}` are not supported. Put the required value in an agreed top-level field. + +Supply the fields required by the event so the Agent does not receive unresolved placeholders. + +## Send one request + +This example uses `demo.ping` created above, or an event with the same name already configured by the administrator, with `payload.message`. Replace the instance address and Agent ID, and set `WEBHOOK_SECRET` through the sender's secret management system. Update the JSON body if your event name or fields differ. + +```bash +BASE_URL='https://your-instance.example' +AGENT_ID='your-agent-id' + +curl --include --request POST \ + "${BASE_URL%/}/api/webhook/${AGENT_ID}" \ + --header "Authorization: Bearer ${WEBHOOK_SECRET}" \ + --header 'Content-Type: application/json' \ + --data '{"event":"demo.ping","payload":{"message":"Test event: please confirm receipt."}}' +``` + +A received event returns `200 OK`: + +```json +{ + "accepted": true, + "agentId": "your-agent-id", + "event": "demo.ping" +} +``` + +`accepted: true` means the event was received and background processing follows. It does not establish that a model has started, a tool call succeeded, or the task completed. The response contains no Run ID, request ID, or final answer. A subsequent background failure cannot change the already returned `200` response. + +## Observe the result + +Before calling, ask the administrator how this event is expected to deliver its result. If the event is authorized to use **SendUserMessage** and calls it during the run, successfully stored messages or attachments appear in the target Agent's conversation in DesireCore connected to the same instance. Reopen that Agent's conversation to load existing deliveries. + +An ordinary final model answer is not automatically delivered there. A missing message may mean the event is still running, sent no message, or encountered a permission or delivery failure. It does not establish success or failure. An existing delivery also does not establish that all subsequent operations in that run completed. + +The instance owner can also read durable delivery records: + +```bash +curl --fail-with-body --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/agents/${AGENT_ID}/schedule-messages?limit=20&offset=0" +``` + +The response is `{"items":[...],"total":...,"limit":20,"offset":0}`. Webhook deliveries have `origin: "webhook"` and the event name in `displayName`; the message is usually in `delivery.message` and attachment references in `delivery.attachments`. A delivery degraded to plain text uses `text`. Only records readable by the current identity are returned. `limit` defaults to 50 and has a maximum of 200; `offset` starts at 0. + +This is the Agent's delivery list, not the terminal status of a specific trigger. Repeated calls to the same event may have interleaved records, and an empty list does not establish execution failure. To identify business events, include the sender's business identifier in the agreed payload and delivered message. This does not add deduplication or a unique Run lookup to the trigger endpoint. + +If the event provides no user delivery, the caller described here receives only the `accepted` receipt feedback; the administrator needs to inspect logs for execution details. This endpoint provides no way to query completion or a final result for this specific request. To investigate a failure or timeout, give the instance administrator the target Agent ID, event name, sending time, and HTTP response so they can inspect service logs. Keep the secret out of troubleshooting records. + +If your application needs a unique execution identifier for every call, terminal status polling, or a result callback, first confirm an appropriate integration method with the administrator. + +## Duplicate calls and timeouts + +This endpoint has no idempotency key or request deduplication. Sending the same request twice may execute it twice. Including a business event ID in `payload` does not automatically enable deduplication. + +If the sender times out or disconnects, receipt may be unknown: the service may already have received the event and started processing it. Save the sending time, event name, and any available response, then check what happened. For events that perform external actions, do not resend unconditionally just because no response arrived. + +Background processing has its own execution timeout controls. Failure or timeout can occur after successful receipt. Increasing the sender's HTTP timeout does not make this asynchronous endpoint return a final result. + +## Management errors + +Management errors use stable identifiers in `error`. Field validation failures may also return `details`. + +| HTTP status | `error` | Action | +| --- | --- | --- | +| `400` | `invalid_webhook_management_request` | Check required fields, types, and limits. Send no body when rotating a secret. | +| `400` | `invalid_webhook_event` | Use a name matching the event naming rules. | +| `400` | `webhook_tool_unavailable` | Read `availableTools` again and choose exact IDs that remain available. | +| `400` | `webhook_events_required` | Save an event and confirm that a secret exists before enabling. | +| `400` | `invalid_agent_id` | Check the target Agent ID. | +| `401` / `403` | Authentication/governance rejection, or `webhook_owner_required` | Use the target instance owner's token. A Webhook secret cannot manage events. | +| `404` | `agent_not_found` / `webhook_event_not_found` | Check the target Agent and the event being deleted. | +| `409` | `webhook_configuration_changed` | This save was not committed. Read configuration and available tools again, confirm permissions, and retry the save. | +| `409` | `webhook_configuration_invalid` | Have the administrator repair existing configuration before saving again. | +| `413` | Save body too large | Reduce the entire event save request to no more than 128 KiB. | +| `500` | `webhook_management_failed` | Ask the administrator to inspect service logs and check whether the change took effect before proceeding. | + +## Trigger errors + +Inspect the HTTP status and the actual `error` field first. Some errors also include `message` or `details`. Do not expect every response to use a fixed `success/data/error` envelope. + +| HTTP status | Error or condition | Caller action | +| --- | --- | --- | +| `400` | `Invalid request body`, possibly with validation `details` | Check that the JSON body is an object, `event` is a string, and `payload` is an object. Remove other top-level fields. | +| `400` | `Unknown event type: ...` | Check the supplied event name, capitalization, and target Agent. | +| `400` | Invalid JSON | Correct JSON syntax before resending. | +| `401` | `Invalid webhook secret` | Check `Authorization: Bearer ...` and the current secret. Confirm that the sender has received any secret rotation. | +| `401` | `Invalid embedded instance token` | This local event lacks the secret needed for external calling. Ask the administrator for an event with a configured secret. | +| `403` | `webhook_secret_required` | The target Agent on the remote instance has no valid Webhook secret. Ask the administrator to complete configuration. | +| `404` | `Agent not found` | Check the instance address and target Agent ID. | +| `404` | `Webhook not enabled for this agent` | Ask the administrator to confirm that events are enabled for the target Agent. | +| `413` | Request body too large | Reduce the entire JSON request to no more than 1 MiB, following the event's payload requirements. | +| `415` | Unsupported media type | Use `Content-Type: application/json`. | +| `500` | `Failed to parse agent.json` | Ask the administrator to check the Agent configuration. Repeated sending cannot repair it. | + +Reverse proxies and connection failures may produce other errors; see [Errors and troubleshooting](./03-errors-and-troubleshooting.md). Fix requests that were explicitly rejected before sending again. Treat a lost response as an unknown outcome as described above. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/03-errors-and-troubleshooting.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/03-errors-and-troubleshooting.md new file mode 100644 index 00000000..23fa516e --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/03-errors-and-troubleshooting.md @@ -0,0 +1,105 @@ +--- +title: Errors and Troubleshooting +description: Distinguish connection, authentication, request validation, and background execution failures when connecting to standalone instances and calling Agent Webhooks. +slug: /api/errors-and-troubleshooting +sidebar_position: 3 +keywords: [API, errors, troubleshooting, Webhook, 401, 403, 503] +--- + +# Errors and Troubleshooting + +When a call fails, check its HTTP status, `Content-Type`, and response body first. Instance authentication, startup status, Webhook handling, and reverse proxies use different error formats. Preserve the actual response instead of assuming that every error has the same `code` field. + +## Connection Failures or HTML Responses + +| Symptom | What to check and do | +|---|---| +| DNS failure, connection refused, or timeout | Confirm the public HTTPS address and actual port; check the instance process, network, and reverse proxy | +| TLS certificate verification failure | Check the domain and certificate trust chain; the calling environment must trust local test certificates | +| `200` with an HTML body | Check the address and path. Request `${BASE_URL}/api/client-handshake` and confirm that the JSON `service` is `desirecore-agent-service` | +| HTML from `/health` on the public address | The default standalone deployment does not expose `/health` as an external API; use the authenticated handshake | +| `404` from an ordinary API request | Check the method, path, and deployment's supported scope; use the public address and avoid connecting to another internal listener | + +Opening an instance page in a browser and requesting its API with curl check different things. Even when the page loads, API calls can fail because of proxy configuration, credentials, or service startup status. + +## Instance Authentication and Startup Errors + +These errors apply to the authenticated handshake in [Connect to an Instance and Authenticate](./01-connection-and-auth.md). + +| HTTP status | Identifier in the body | Meaning and action | +|---|---|---| +| `401` | `error: "unauthorized"` | `X-Agent-Service-Token` is missing or invalid. Confirm that the token belongs to the target instance and that the caller is not using a value from before token rotation | +| `403` | `error: "origin_not_allowed"` | The deployment does not allow the calling page's origin. Have the deployment owner check the exact origin in `AGENT_SERVICE_CORS_ORIGIN`; an allowed origin does not replace a token | +| `503` | `code: "service_not_ready"` | The service is starting. Wait as directed by `Retry-After`, then retry the read-only handshake; inspect startup logs if the failure persists | +| `503` | `error: "instance_token_uninitialized"` | The instance access token is not initialized. The deployment owner should check `AGENT_SERVICE_TOKEN` in the startup configuration; normal standalone startup requires it | +| `500` | Message indicating that the instance identity file cannot be read | The deployment owner should check data directory permissions and structure; preserve existing instance data and do not attempt recovery by deleting its identity file | + +Example instance authentication failure: + +```json +{ + "error": "unauthorized", + "message": "缺少或无效的实例访问令牌,请在请求头 X-Agent-Service-Token 中提供。" +} +``` + +Example startup response: + +```json +{ + "status": "starting", + "service": "agent-service", + "code": "service_not_ready" +} +``` + +Treat startup and authentication separately. An invalid token can return `401` even while the service is starting. Wait for startup only when the response is `service_not_ready`. Browser cross-origin restrictions can also prevent a script from reading the response; inspect the request and preflight result in the browser's network panel. + +## Webhook Request Errors + +An external [Agent Webhook](./02-agent-webhook.md) uses the target Agent's secret supplied by an administrator in `Authorization: Bearer `. The administrator must have configured and verified the event before you call it. Do not use a provider API key or another Agent's secret for this endpoint. + +| HTTP status | `error` in the body or symptom | Meaning and action | +|---|---|---| +| `401` | `Invalid webhook secret` | The secret is missing or does not match. Check the target Agent and `Authorization`, preserving the exact capitalization and spacing of `Bearer ` | +| `403` | `webhook_secret_required` | The target standalone Webhook has no valid configured secret. Ask the administrator to check and configure the Agent's dedicated secret | +| `404` | `Agent not found` | The specified Agent does not exist; check the target instance and Agent ID | +| `404` | `Webhook not enabled for this agent` | The target Agent's Webhook is disabled; ask the administrator to confirm that the event is enabled before sending | +| `400` | `Invalid request body` | The request does not meet field requirements. Correct it using the response `details` and endpoint parameter documentation | +| `400` | `Unknown event type: ...` | No configuration matches `event`; use the exact event name delivered by the administrator | +| `400` | JSON parsing error | Fix JSON syntax, such as comments, trailing commas, or unescaped characters | +| `413` | Request body too large | The Webhook body exceeds `1 MiB`; reduce the entire encoded body in bytes, including `event` and `payload` | +| `415` | Unsupported request media type | Use `Content-Type: application/json` as shown in the endpoint example | +| `500` | `Failed to parse agent.json` | The target Agent configuration cannot be parsed; ask the administrator to correct its JSON errors | + +Example Webhook secret validation error: + +```json +{ + "error": "Invalid webhook secret" +} +``` + +For a field validation failure, the `Invalid request body` response includes validation information in `details`. The request framework handles JSON parsing, media types, and body size limits, so those errors may use fields such as `statusCode`, `error`, and `message`. A reverse proxy may also return plain text or HTML. Check the response type before reading its body. + +## A Trigger Was Accepted but No Result Arrived + +A Webhook `200` with `accepted: true` means the trigger was accepted. The response contains no Run ID or final result. Background execution failures do not change that HTTP response. + +Ask the administrator to check that the event matched the intended prompt, has execution permissions, and includes a result delivery step. An ordinary final answer does not automatically appear in chat or become the Webhook HTTP response. For chat notifications, the administrator must arrange and authorize a message delivery step; successfully delivered messages appear in the target Agent's conversation. Receiving no message does not establish whether the event ran, failed, or completed. See [Agent Webhook](./02-agent-webhook.md) for observation methods. The administrator must inspect instance execution logs to investigate background failures. + +After a timeout or lost connection, the sender may not know whether the service accepted the request. The current Webhook has no public idempotency key, so resending can start another execution. Check delivered messages and ask the administrator to check execution logs before deciding to resend; do not configure unconditional automatic retries. A read-only handshake can be retried as directed by its startup `Retry-After` header. + +## Collect Troubleshooting Information + +When requesting help, record the request time, HTTP method and path, status, response type, redacted body, the handshake's `serverVersion` and `instanceId`, and whether the issue appeared after updating a token. Remove instance tokens, Webhook secrets, provider API keys, and business data before sharing log excerpts. + +## Webhook configuration conflicts and permissions + +Configuration management requires instance owner access and `capabilities.webhookManagement: true` in the handshake. Management requests use the instance token; trigger requests use the Webhook secret. Follow the [configuration steps](./02-agent-webhook.md). + +`409 webhook_configuration_changed` means the Agent's permissions or other configuration changed during setup. Reload available tools and existing events, review permissions, and save again. `400 webhook_tool_unavailable` means a tool ID is absent or ambiguous; select from the latest management GET's `availableTools`. `409 webhook_configuration_invalid` requires the administrator to repair malformed existing configuration. + +## Conversations, proxy and workflow troubleshooting + +After a conversation timeout, use the example's `--recover` to read admission records. Preserve request IDs, digest and state files; an unknown outcome does not prove execution never started. See [conversations and Runs](./04-conversations-and-runs.md). For model calls, check the local Base URL and provider binding of the safe token. For workflows, check the independent address and `X-Workflow-Service-Token`, then follow the [workflow guide](./06-workflow-integration.md) to inspect the `runId` and terminal status. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/04-conversations-and-runs.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/04-conversations-and-runs.md new file mode 100644 index 00000000..cb39c687 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/04-conversations-and-runs.md @@ -0,0 +1,425 @@ +--- +title: Conversations and Runs +description: Submit and follow up through HTTP and Socket.IO, query Agents and Run receipts, and reconcile unknown admission, retries, and disconnections. +slug: /api/conversations-and-runs +sidebar_position: 4 +keywords: [API, Agent, Run, Receipt, conversation, runtime state, pagination] +--- + +# Conversations and Runs + +Deployment owners can query Agents and execution records in their own standalone instance to select a target Agent, observe a known Run, and read execution receipts. See [Connect to an Instance and Authenticate](./01-connection-and-auth.md) for the address and instance token. All read requests below use `X-Agent-Service-Token`. + +## Read Scope + +The Agent list, Run list, Run details, and runtime state are instance data reads for the instance owner. They do not provide per-user isolation between multiple tenants. Receipt reads additionally match the trusted identity resolved by the server to the Run's user ownership; both a missing Run and a Run belonging to another user return `404`. + +Agent IDs, Run IDs, `user_id`, and `conversation_id` identify records; they are not access credentials. Supplying a user ID, knowing a Run ID, or having seen a record elsewhere does not replace authentication and the endpoint's ownership checks. + +## List Agents {#list-agents} + +### `GET /api/agents` + +No request body, pagination, or filter parameters. A successful response is `{ "agents": [...] }`. Items are summaries rather than full Agent configurations. + +```bash +curl --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/agents" +``` + +Example response structure: + +```json +{ + "agents": [ + { + "id": "your-agent-id", + "name": "Example Agent", + "skillsCount": 0, + "toolsCount": 0, + "heartbeatEnabled": false, + "hasGit": false, + "isCoreAgent": false, + "status": "idle" + } + ] +} +``` + +| Field | Type and meaning | +|---|---| +| `id`, `name` | Required strings; use `id` for other API calls, not the display name | +| `skillsCount`, `toolsCount` | Required nonnegative numbers; skill and tool counts in the summary | +| `heartbeatEnabled`, `hasGit`, `isCoreAgent` | Required booleans for heartbeat, a Git repository, and core Agent identity | +| `status` | Optional: `online`, `busy`, `idle`, `offline`, `error`, `recovery`, or `shutdown` | +| `metadataError` | Optional: `missing`, `unreadable`, `invalid_json`, or `invalid_shape`; names, descriptions, and counts may be fallback values when present, so ask the administrator to repair configuration | +| `description`, `engine`, `hasRemote`, `syncStatus` | Optional description, engine, upstream repository, and synchronization information | +| `ui`, `avatar`, `avatarImage` | Optional UI and avatar summaries; configured avatar metadata does not establish that its file is usable | +| `unread`, `pinned`, `pinnedAt`, `folded`, `groupOverride`, `nestMembers` | Optional unread information and display preferences for the instance's current user; do not fabricate observed values for absent fields | + +## List Runs {#list-runs} + +### `GET /api/runs` + +Runs are ordered by `started_at`, newest first. Cleared conversation records are excluded by default. Filters apply to all readable records before taking up to `limit` entries. Multiple filters are combined with AND. + +| Query parameter | Type | Default and meaning | +|---|---|---| +| `limit` | One decimal positive integer string | Defaults to `50`; must convert to a safe integer, with no additional universal count cap | +| `agent_id` | string | Optional: the Agent that executed the Run | +| `delegated_by` | string | Optional: the delegating Agent, matching `delegation.fromAgentId` | +| `parent_run_id` | string | Optional: the parent delegation Run, matching `delegation.parentRunId` | + +This endpoint provides no `offset`, cursor, or `total`. Do not apply another endpoint's pagination fields. Empty, negative, fractional, mixed, repeated, or unsafe integer `limit` values return `400`. + +Obtain `AGENT_ID` from the Agent list, then query that Agent's Runs: + +```bash +AGENT_ID='your-agent-id' + +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode "agent_id=${AGENT_ID}" \ + --data-urlencode 'limit=20' \ + "${BASE_URL}/api/runs" +``` + +```json +{ + "runs": [ + { + "run_id": "00000000-0000-4000-8000-000000000001", + "user_id": "example-owner", + "agent_id": "your-agent-id", + "started_at": "2026-10-14T01:00:00.000Z", + "status": "idle" + } + ] +} +``` + +Each `runs` item contains the metadata described below. This list does not replay every realtime event or associate completion with a particular Webhook request. + +## Read Run Details {#get-run} + +### `GET /api/runs/:id` + +Use an existing Run ID for `:id`. There is no request body or additional query parameter. Success returns the Run metadata object directly, without a `run` or `data` wrapper. + +```bash +RUN_ID='replace-with-an-existing-run-id' + +curl --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/runs/${RUN_ID}" +``` + +```json +{ + "run_id": "00000000-0000-4000-8000-000000000001", + "user_id": "example-owner", + "agent_id": "your-agent-id", + "started_at": "2026-10-14T01:00:00.000Z", + "status": "idle" +} +``` + +| Field | Meaning | +|---|---| +| `run_id`, `user_id`, `agent_id` | Required strings identifying the Run, initiating user, and executing Agent | +| `started_at` | Required ISO 8601 creation time; a Run can be reused across multiple turns | +| `status` | Required: `running`, `idle`, `completed`, or `failed` | +| `conversation_id`, `session_id` | Optional conversation and realtime execution session identifiers; do not confuse them with one submission's request ID | +| `turn_started_at`, `completed_at`, `updatedAt` | Optional current turn start, Run completion, and latest activity times | +| `failure_reason` | Failure reasons include `process_restart`, `timeout`, `stale`, and `error` | +| `interruption_reason`, `interrupted_at` | Optional interruption reason and time; user stops, system suspension, and background timeouts have distinct identifiers | +| `delegation` | Optional delegation lineage, including `fromAgentId` and possibly `parentRunId`, `parentClientRequestId`, and `mode` | +| `background`, `inbound` | Optional current turn identity, for background processing and a message from another Agent respectively | +| `title`, `messageCount`, `archived_at`, `cleared` | Optional title, message count, and history lifecycle information | +| `team_id`, `work_context_id`, `execution_attachment` | Optional execution ownership information; reading these fields grants no resource permissions | + +Responses may also include optional context, recovery, and working directory metadata. An absent field means that information is not recorded; do not invent completion times, failure reasons, or ownership. + +`idle` means the Run is not currently executing and can await another turn. It does not establish business success; an interrupted Run can also return to `idle`. `completed` and `failed` describe the Run lifecycle. To evaluate a particular submission, associate it with that turn's terminal event and receipt. + +Some historical branches can appear in the list while this detail endpoint returns `404`. Use the endpoints below when you need runtime state or receipts, and handle their own read results separately. + +## Read Runtime State {#get-run-runtime-state} + +### `GET /api/runs/:id/runtime-state` + +Returns a persisted Run state snapshot with support for historical branches. `:id` accepts only letters, digits, underscores, and hyphens. A successful response contains exactly three fields: + +```bash +curl --silent --show-error \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + "${BASE_URL}/api/runs/${RUN_ID}/runtime-state" +``` + +```json +{ + "run_id": "00000000-0000-4000-8000-000000000001", + "agent_id": "your-agent-id", + "status": "idle" +} +``` + +`status` accepts only `running`, `idle`, `completed`, and `failed`. An unreadable or unsupported state returns `404`. A stored `running` value is preserved, but this is still a snapshot. It grants no permission to cancel, rewind, or otherwise modify the Run, and the state can change before a subsequent submission. + +## Read Execution Receipts {#list-run-receipts} + +### `GET /api/runs/:id/receipt` + +Receipts contain turn inputs, outputs, tool calls, and outcomes. The endpoint resolves a trusted user from the instance credential; it accepts no `user_id` parameter as a substitute for identity checks. A Run belonging to another user returns `404`, even if another read endpoint returned its metadata. + +Without `offset` or `limit`, the response is `{ "run_id": "...", "receipts": [...] }`, with existing receipts in write order. Prefer pagination to bound each read. Providing either pagination parameter enables these rules: + +| Query parameter | Default | Effective range and handling | +|---|---|---| +| `offset` | `0` | One nonnegative decimal integer string; skips from the latest receipt backwards, capped at `10000` | +| `limit` | `20` | One positive decimal integer string, capped at `100` | + +Both parameters must be valid before and after conversion. Empty, fractional, negative, repeated, mixed, and unsafe integer values return `400`. Response `offset` and `limit` contain the effective values. Receipts within a page remain in write order, so its last item is a newer receipt in that page. + +```bash +curl --silent --show-error --get \ + --header "X-Agent-Service-Token: ${AGENT_SERVICE_TOKEN}" \ + --data-urlencode 'offset=0' \ + --data-urlencode 'limit=20' \ + "${BASE_URL}/api/runs/${RUN_ID}/receipt" +``` + +```json +{ + "run_id": "00000000-0000-4000-8000-000000000001", + "receipts": [ + { + "receipt_id": "example-receipt-id", + "run_id": "00000000-0000-4000-8000-000000000001", + "goal": "Reply to the test message", + "input": "Please say hello.", + "output": "Hello.", + "tool_calls": [], + "outcome": { "status": "success", "auto_completed": true }, + "started_at": "2026-10-14T01:00:00.000Z", + "completed_at": "2026-10-14T01:00:01.000Z" + } + ], + "total": 1, + "offset": 0, + "limit": 20, + "has_more": false, + "skipped": 0 +} +``` + +| Field | How to use it | +|---|---| +| `receipt_id` | Stable identifier of one receipt; a Run can have multiple receipts | +| `goal`, `input`, `output`, `tool_calls` | Goal, input, output, and tool calls; these can contain business data | +| `outcome.status` | `success`, `partial`, or `failed`, describing that receipt's outcome | +| `outcome.auto_completed` | Whether completion required no human intervention | +| `started_at`, `completed_at` | That receipt's start and completion times | +| `usage`, `duration_ms`, `interruption_reason` | Optional usage, duration, and interruption information; absence does not establish zero usage or normal completion | +| `total`, `has_more` | Pagination count and whether older records remain; absent without pagination parameters | +| `skipped` | Damaged or oversized records skipped during a paged read, not a business failure count | + +Historical records can also be skipped during Schema validation. Thus `receipts.length` can be smaller than the number of records traversed, and `total` is not the array length. Advance to older pages using response `offset + limit`, not `receipts.length`. Stop at the `10000` offset cap to avoid rereading the same page. + +Empty receipts, skipped historical entries, or an older last receipt do not independently establish completion of the current turn. Reconcile the target Run, turn timing, and any available turn correlation information. Keep the outcome unknown when evidence is insufficient. + +## Read Errors {#read-errors} + +| Endpoint or scope | HTTP status and body | Action | +|---|---|---| +| All endpoints | `401`, `error: "unauthorized"` | Check the instance token and target address | +| All endpoints | `403 origin_not_allowed` or `503 service_not_ready` | Follow [Errors and Troubleshooting](./03-errors-and-troubleshooting.md) for origin restrictions or startup waiting | +| Run list and paged receipts | `400`, with `error` starting `Invalid pagination:` | Use one valid decimal integer per parameter | +| Run details | `404`, `error: "Run not found: ..."` | Check the Run ID and detail read scope | +| Runtime state and receipts | `400`, `error: "Invalid run ID"` | Use a safe Run ID returned by the service; do not construct a path | +| Runtime state | `404`, `error: "Run runtime state not found: ..."` | No supported runtime state is readable; do not assume `idle` | +| Receipts | `401`, `error: "无法解析用户身份"` | Ask the administrator to check instance identity configuration | +| Receipts | `404`, `error: "Run not found: ..."` | Missing, unreadable, or differently owned Run; do not bypass ownership checks | +| Read failure | `500` | Save the redacted response and ask the administrator to inspect service logs; use the actual returned error body | + +Query parameters do not change execution permissions. Read endpoints can be queried again to update observations. Do not automatically turn a failed read or missing record into a new task submission. + +## Start and Continue a Conversation {#submit-conversation} + +Submit through HTTP and receive progress and turn termination through Socket.IO. Use an Agent with its model and execution permissions configured. HTTP and Socket.IO must connect to the same instance using an instance token for the same identity. + +### Run the Node.js Example {#node-client-example} + +[Download the client example](pathname:///api-examples/agent-client.mjs) and save it as `agent-client.mjs`. It demonstrates text submission, sequential follow-ups, persistent request information, reconnection checks, and explicit cancellation. Use **Node.js 22+** and install the dependency in the separate directory containing the example: + +```bash +npm init -y +npm install socket.io-client@4.8.4 +``` + +Set your instance address, existing token, and an Agent ID obtained from the list: + +```bash +export DESIRECORE_URL='https://agent.example.com' +export DESIRECORE_TOKEN='replace-with-your-existing-instance-token' +export DESIRECORE_AGENT_ID='your-agent-id' + +node agent-client.mjs \ + --prompt 'Please say hello.' \ + --follow-up 'Please reply in another language.' \ + --state ./request.json +``` + +You can repeat `--follow-up`. The example attempts the next turn only after observing explicit completion and confirming through admission lookup that the session belongs to the current service process. An initial call does not overwrite an existing state file; use a new `--state` path for a separate task. The file persists original requests, business text, and correlation information without storing the instance token. Handle it as business data. + +The program verifies the handshake, registers event listeners, and connects through Socket.IO. Its path is `/socket.io/` and authentication is `auth: { token, clientInstanceId }`, with the same `token` as the HTTP `X-Agent-Service-Token` header. `clientInstanceId` is a stable 16–128 character identifier containing only letters, digits, `.`, `_`, `:`, and `-`. Authentication binds the client receiving channel; the example does not send an additional subscription message. + +### HTTP Submission Fields + +`POST /api/query/start` accepts JSON. Its top level and `options` reject undeclared fields. Provide a nonempty `prompt` or valid `mediaRefs`. The text example uses these fields: + +| Field | Purpose and example convention | +|---|---| +| `prompt` | User text for this turn; the example sends a nonempty string | +| `options.agentId` | Target Agent ID | +| `options.conversationId` | Conversation scope, saved as a stable value and reused for subsequent requests | +| `clientRequestId` | Stable ID for one submission; preserve it for recovery or bounded replay, and use a new ID for another turn | +| `userMessageId` | Stable user message ID for this turn, saved with the request | +| `requestDigest` | Request content digest beginning `qrd1:`, calculated by the example according to the protocol; it is not an access credential | +| `originSocketId` | Current authenticated `socket.id`; update it after reconnection rather than reusing an old connection ID | + +Reliable correlation and recovery need these stable identifiers. See [OpenAPI Downloads](./07-openapi.md) for complete fields and constraints. Adding undeclared permission objects or user identity fields cannot expand authorization. + +An admitted new request returns `202`: + +```json +{ + "ok": true, + "clientRequestId": "00000000-0000-4000-8000-000000000002" +} +``` + +A duplicate admission response can also contain an existing `sessionId`. `202` acknowledges admission; it does not prove that the user message was durably committed, a model executed, or the turn completed. Events such as `query:started` can arrive before the HTTP response. Register listeners before submitting, and correlate both channels by request ID. + +`POST /api/query/follow-up` continues a known execution session with a different request shape: + +| Field | Purpose and example convention | +|---|---| +| `sessionId` | Reusable execution session ID returned by the service | +| `originSocketId` | Current authenticated Socket.IO connection ID | +| `message.id` | Stable user message ID for this turn | +| `message.role` | `"user"` | +| `message.content` | Turn text | +| `message.timestamp` | User message time as a millisecond timestamp | +| `clientRequestId` | Stable request ID for the new turn | +| `requestDigest` | The turn's `qrd1:` digest calculated and saved by the example | + +An admitted follow-up returns `202` with `ok: true`, `sessionId`, and the corresponding `clientRequestId`. Duplicate admission can include `duplicate`. Do not submit the start request's `prompt/options` object as a follow-up. + +### Receive Events and Determine Turn Termination {#conversation-events} + +| Socket.IO event | Main fields | Action | +|---|---|---| +| `query:started` | `sessionId`, possibly `runId`, `agentId`, `conversationId`, and `clientRequestId` | Save assigned execution identifiers and associate them with the submission | +| `query:followUpAccepted` | `sessionId`, possibly `clientRequestId` | Associate follow-up admission information; it is not completion | +| `query:message` | `sessionId`, `message`, possibly request and conversation identifiers | Observe text, tools, or deliveries; `message` varies by message type | +| `query:complete` | `sessionId`, possibly `clientRequestId`, `terminal`, and `result` | Correlate the request and inspect structured `terminal`; preserve unknown status without terminal information | +| `query:error` | `error`, possibly `code`, request and execution identifiers | Keep associated errors; do not attribute an uncorrelated error to another request | +| `activity:run_status` | `runId`, `status`, possibly `sessionId` | Update Run observations; this is not a single submission's terminal status | + +`query:complete.terminal.status: completed` means the turn ended normally. `interrupted` means it was interrupted; inspect available `reason`, `partial`, and `runId` information. Evaluate business success using output and receipts as well. A `result`, an explicit delivery message, or an `idle` Run cannot replace the turn's terminal status. + +Without a matching `clientRequestId` or structured `terminal`, the example does not declare that this request succeeded. Its state file retains observed information for reconciliation. + +### Distinguish Identifiers {#conversation-identifiers} + +| Identifier | Scope and lifetime | +|---|---| +| `clientInstanceId` | Client receiving channel; reused when recovering the same example state | +| `originSocketId` | One Socket.IO connection; changes on reconnection | +| `clientRequestId` | One start or follow-up submission; unchanged for replay, different for each new request | +| `userMessageId` / `message.id` | This turn's user message, at different field locations in start and follow-up | +| `conversationId` | Conversation scope, reusable across turns | +| `sessionId` | Service-assigned execution session; reuse only when the service confirms it remains usable | +| `runId` | Persistent execution record, reusable across turns with separate receipts | +| `instanceId` | Data-space identity from the handshake, used to verify the target instance | +| `serverInstanceId` | Service process generation from admission lookup; changes on restart and is neither a data-space identity nor a credential | + +## Unknown Admission, Retries, and Reconnection {#request-reconciliation} + +After a timeout, lost response, or disconnection, preserve the original request IDs, digest, and dispatch time. Use read-only admission lookup to observe its status before deciding whether bounded replay is allowed. Do not generate a new request ID just to retry. + +### `POST /api/query/client-requests/lookup` + +This authenticated, read-only operation does not invoke a model. Its JSON top level and each `requests` item reject undeclared fields: + +```json +{ + "agentId": "your-agent-id", + "conversationId": "your-agent-id", + "requests": [ + { + "clientRequestId": "00000000-0000-4000-8000-000000000002", + "userMessageId": "00000000-0000-4000-8000-000000000003" + } + ] +} +``` + +`agentId` is required and `conversationId` optional. `requests` contains 1–100 items, each with a required `clientRequestId` and optional `userMessageId`. IDs are nonempty strings of at most 256 characters. Success includes `protocolVersion: 1`, `serverInstanceId`, `observedAt`, `autoReplayRetentionMs`, `tombstoneRetentionMs`, and `results`. + +| `results[].status` | Meaning and action | +|---|---| +| `committed` | Committed; do not replay. May include `committedAt`, `sessionId`, `committedServerInstance`, and `executionWitness`; it does not establish completion | +| `reserved` | Reserved; wait for a definite outcome rather than sending another concurrent request | +| `absent` | No confirmable record at observation time; consider bounded replay only within the service's protection window, preserving the original IDs and identical request content | +| `unknown` | Cannot be determined reliably; stop automatic replay and keep the outcome unknown while investigating | + +`committedServerInstance` is `current`, `previous`, or `unknown`, describing the relationship to the service process generation. `executionWitness` is `user-message`, `none`, or `unknown`, describing observed execution evidence. Neither committed admission nor generation and evidence fields establish business completion. + +The example calculates request age using the response protection windows and server `observedAt`, without hardcoding a permanent retry period. Even `absent` does not permit automatic replay outside the window. `CLIENT_REQUEST_BODY_MISMATCH` indicates a content digest mismatch for the same request ID; stop replay and inspect the saved original request. Treat a new task separately from recovery of the original request. + +### Resume or Inspect Without Submitting + +Resume a saved request: + +```bash +node agent-client.mjs --resume ./request.json +``` + +The example preserves the original IDs and content and updates only `originSocketId` after reconnection. It does not replay committed requests, keeps observing reserved requests, and stops automatic replay for unknown or expired requests. Recovery does not replace an unusable original session with a new start request. The URL and handshake instance identity must match the saved target. + +Inspect admission and readable execution records without submitting: + +```bash +node agent-client.mjs --recover ./request.json +``` + +This mode establishes no Socket.IO subscription and sends no start or follow-up. With a known `runId`, it reads runtime state and receipts. With only `sessionId`, it searches for an exact Run match rather than guessing that the latest Run is this execution. These observations cannot replay all missed events or automatically repeat external actions. The outcome remains unknown when this turn's terminal evidence is missing. + +Exit codes are `0` for an observed completed turn, `1` for rejection, failure, or interruption, and `2` for an unknown outcome or read-only recovery mode. `--recover` still returns `2` after completing its read-only checks; this does not mean the task was resubmitted or confirmed to have failed. + +### Cancel Explicitly + +```bash +node agent-client.mjs --cancel ./request.json +``` + +When no terminal outcome is recorded, cancellation sends Socket.IO `query:abort` with known session, conversation, request, and Agent identifiers. There is no synchronous acknowledgment. Wait for a correlated interrupted terminal in `query:complete`; without confirmation, preserve an unknown outcome. If a terminal outcome is already recorded, the example reports it without sending cancellation again. `--resume`, `--recover`, and `--cancel` are mutually exclusive and cannot be combined with `--prompt` or `--state`. Ctrl+C stops the example client without implicitly cancelling the server task. + +### Submission and Recovery Errors + +| HTTP status or identifier | Action | +|---|---| +| `400`, invalid submission or lookup fields | Correct the corresponding request shape; lookup returns `client request lookup body does not match schema` | +| `401` / `403` | Check the instance token, page origin, matching HTTP/Socket identity, and target connection; do not alter markers to bypass authentication | +| `409`, `busy: true` and `retryable: true` | The conversation is being cleared; wait for clearing to finish, then retry with the original request ID and content | +| `409 CLIENT_REQUEST_BODY_MISMATCH`, `retryable: false` | The saved ID and request content differ; stop replay and check the original saved request | +| `503`, unavailable connection, request registry, or admission confirmation | Preserve the actual `retryable` and error information and inspect admission first; not every `503` permits direct resubmission | +| Lost response, disconnection, or no correlated terminal | Reconcile through read-only checks first; keep unknown status and stop automatic replay if unconfirmed | + +## Related Reference + +- [OpenAPI Downloads](./07-openapi.md): public HTTP operations, parameters, and response Schemas. +- [Errors and Troubleshooting](./03-errors-and-troubleshooting.md): connection, authentication, startup, and asynchronous failures. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/05-model-proxy.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/05-model-proxy.md new file mode 100644 index 00000000..0b3697fc --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/05-model-proxy.md @@ -0,0 +1,206 @@ +--- +title: Model Protocol Proxy +description: Obtain a local model proxy address and a Provider-bound safe token, call Messages, Chat Completions, and Models, and handle streaming and compatibility limits. +slug: /api/model-proxy +sidebar_position: 5 +keywords: [API, Compat Proxy, model proxy, safe token, Provider, Messages, Chat Completions] +--- + +# Model Protocol Proxy + +DesireCore **Compat Proxy** lets local programs call configured model providers using Messages or Chat Completions requests. Callers use a safe token beginning `desirecore_`; the proxy obtains the corresponding provider credential and forwards or converts the request. + +This page covers trusted programs on the same machine as the proxy. The default listener is `127.0.0.1` with a dynamically assigned port. Its address and credentials differ from Agent Service `/api` and its instance token. The request body limit is `256 MiB`. + +## Obtain the Address, Model, and Safe Token {#proxy-credentials} + +1. Configure and verify the provider in [Compute Services](../02-user-guide/10-settings/04-compute-service.md). +2. Ask a trusted Agent in DesireCore to call `InspectModels` and check the exact **Provider ID** and `modelName`. Identically named models can belong to different providers, so use the specific configuration ID. +3. Ask the Agent to call `ComputeCredential` with `action: "list"`, check providers with available credentials, and obtain the current local proxy Base URL. It already includes `/v1`. +4. Call `ComputeCredential` for the chosen provider with `{"action":"get","provider":""}`, without `raw: true`. The default result is a revocable `desirecore_` handle. +5. Have the Agent inject the handle into the protected local program configuration you specify, then verify a small request below. Tool cards may show a sensitive-result placeholder, and the Safe Token panel displays only identifying fragments. Neither is a full-token copy interface. + +For example, ask: + +> Inspect my configured chat models and confirm the Provider ID and modelName. Use ComputeCredential list to obtain the current local proxy Base URL, get a default safe token for the selected provider, inject it into my specified local program configuration, and verify a request that replies only with OK. + +If the tool reports that the proxy is not running, restore the service first. If an existing instance's tool output contains no address, ask the administrator for its verified current loopback address. `InspectModels` metadata does not replace the proxy address, and the upstream provider `baseUrl` cannot accept this handle. + +| Example variable | Source and meaning | +|---|---| +| `PROXY_BASE_URL` | Current local Base URL from the tool or administrator, already including `/v1`, without a trailing slash | +| `PROXY_TOKEN` | Full safe handle returned by default `ComputeCredential get` | +| Model name | Exact `modelName` from the chosen provider; replace example placeholders | + +A safe token binds to the **Provider configuration ID** chosen at issuance. An `x-desirecore-provider-id` header cannot override that binding. If the bound provider does not serve the requested model, the proxy does not fall back to another provider. This binding is not a model-level allowlist, request budget, or automatic expiration policy. + +The handle is a usable credential and belongs in protected local configuration. Any program that can reach this local entry point and holds the handle can use its bound provider. The issuing Agent name in a record does not mean every HTTP request reauthenticates that program's identity. + +## List Models {#proxy-models} + +### `GET /v1/models` + +Authenticate with a Bearer token or `x-api-key`. No request body or pagination parameters: + +```bash +PROXY_BASE_URL='replace-with-the-returned-local-base-url' +PROXY_TOKEN='replace-with-your-desirecore-safe-token' + +curl --silent --show-error \ + --header "Authorization: Bearer ${PROXY_TOKEN}" \ + "${PROXY_BASE_URL}/models" +``` + +```json +{ + "object": "list", + "data": [ + { + "id": "your-model-name", + "object": "model", + "created": 1791943200, + "owned_by": "openai" + } + ] +} +``` + +`id` is the model name. `owned_by` is a provider type, not a Provider configuration ID. `created` is a Unix timestamp generated during listing, not the model's release time. + +The current list includes configured models from every enabled provider and **is not filtered to the token's bound provider**. A listed model does not establish that this token can call it or that it is currently available. Calls remain subject to provider binding, configuration, and upstream state. Check your target using the Provider information from `InspectModels`. + +## Messages Requests {#proxy-messages} + +### `POST /v1/messages` + +Use Anthropic Messages format. This example sends text without streaming and sets an output limit of `64`: + +```bash +curl --silent --show-error --include --request POST \ + --header "x-api-key: ${PROXY_TOKEN}" \ + --header 'anthropic-version: 2023-06-01' \ + --header 'Content-Type: application/json' \ + --data '{"model":"your-model-name","max_tokens":64,"stream":false,"messages":[{"role":"user","content":"Reply only with OK."}]}' \ + "${PROXY_BASE_URL}/messages" +``` + +| Field | Purpose | +|---|---| +| `model` | Exact model name from the bound provider | +| `messages` | Messages array; the example contains one user text message | +| `max_tokens` | Messages output limit | +| `system` | Optional system prompt, as a string or protocol-compatible system blocks | +| `stream` | Optional boolean, default `false`; `true` requests SSE | +| `temperature`, `top_p`, `stop_sequences` | Optional generation parameters subject to model constraints | +| `tools`, `tool_choice`, `thinking`, `output_config` | Optional tools and reasoning/output settings, subject to conversion and backend support | + +Example nonstreaming response structure: + +```json +{ + "id": "msg_example", + "type": "message", + "role": "assistant", + "content": [{ "type": "text", "text": "OK" }], + "model": "your-model-name", + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { "input_tokens": 6, "output_tokens": 1 } +} +``` + +Read `content` and `stop_reason`. For example, `tool_use` means the model requested a tool call, not that the tool ran. Your program must execute permitted tools and submit their results according to the protocol. A stop reason such as `max_tokens` does not establish business completion. + +## Chat Completions Requests {#proxy-chat-completions} + +### `POST /v1/chat/completions` + +Use OpenAI Chat Completions format: + +```bash +curl --silent --show-error --include --request POST \ + --header "Authorization: Bearer ${PROXY_TOKEN}" \ + --header 'Content-Type: application/json' \ + --data '{"model":"your-model-name","max_completion_tokens":64,"stream":false,"messages":[{"role":"user","content":"Reply only with OK."}]}' \ + "${PROXY_BASE_URL}/chat/completions" +``` + +| Field | Purpose | +|---|---| +| `model`, `messages` | Target model and Chat Completions message array | +| `max_completion_tokens` | Optional output limit subject to model and backend constraints | +| `stream`, `stream_options` | Optional streaming settings; usage availability depends on the backend | +| `temperature`, `top_p`, `stop` | Optional generation parameters | +| `tools`, `tool_choice`, `parallel_tool_calls` | Optional function tool definitions and selection; conversion does not execute tools for your program | +| `reasoning_effort`, `response_format` | Optional reasoning/output constraints subject to model and conversion backend support | + +```json +{ + "id": "chatcmpl_example", + "object": "chat.completion", + "created": 1791943200, + "model": "your-model-name", + "choices": [ + { + "index": 0, + "message": { "role": "assistant", "content": "OK" }, + "finish_reason": "stop" + } + ], + "usage": { "prompt_tokens": 6, "completion_tokens": 1, "total_tokens": 7 } +} +``` + +Read `choices[].message`, `finish_reason`, and actual usage fields. Handle finish reasons such as `tool_calls` and `length` separately. Missing usage is not zero consumption. + +## Streaming and Protocol Scope {#proxy-streaming} + +Set `stream: true` and read with an SSE-capable client; curl can use `--no-buffer`. Messages and Chat Completions return their corresponding protocol streams. The proxy forwards or converts according to the provider's configured API format, so these are not general replacements for complete provider APIs. + +Neither HTTP `200` nor the first text fragment establishes normal stream completion. Read the protocol's terminal markers and inspect in-stream errors. On a disconnection, timeout, empty response, or missing termination information, retain partial content and report incomplete execution. Errors can occur after headers are sent, so checking the initial HTTP status is insufficient. + +Cross-protocol tools and structured output depend on the converter and backend. Subscription backends can reject unsupported Schemas, tool choices, and parameters. Internal DesireCore tool markers, beta parameters, and complex Schemas are not universally supported external inputs. Start with the small text request here, then verify each additional capability against your backend. + +The documented inbound endpoints are `/v1/models`, `/v1/messages`, and `/v1/chat/completions`. There is no inbound `/v1/responses` contract here; a provider using Responses does not imply an identically named proxy endpoint. + +## Update and Revoke Tokens {#proxy-token-lifecycle} + +Safe handles have no automatic expiry. They can be reused within an execution session and do not become invalid merely because a session ended or the service restarted. The port can change on restart, so obtain the current proxy address again. + +Use the compute resource **Safe Token** panel to revoke an entry or invalidate all active handles, then obtain a replacement and update callers. If the panel reports that management is unsupported in this environment, use the administrator's supported desktop management entry point. The panel is neither a token issuer nor a full-handle copy interface. + +Revocation affects subsequent authentication and does not guarantee cancellation of requests already forwarded. Disabled providers, removed models, login state, upstream credential refresh, and quota limits can also cause later calls to fail. Diagnose those separately from safe-handle revocation. + +## Errors and Eligibility {#proxy-errors} + +Authentication failure uses this shape even for Chat Completions requests: + +```json +{ + "type": "error", + "error": { + "type": "authentication_error", + "message": "Unauthorized compat-proxy request" + } +} +``` + +| Status or condition | Action | +|---|---| +| `401 authentication_error` | Check the local address, handle, and revocation; neither an instance token nor a real provider key substitutes for the safe handle | +| `400 Missing required field: model` | Use the exact model name from the bound provider | +| `400`, bound provider does not serve the model | Check the provider, enablement, and model configuration; the proxy will not switch providers | +| Rejected parameters or tool Schema | Correct according to actual errors and backend compatibility; internal request markers cannot bypass restrictions | +| `413` / `415` | Keep the body within `256 MiB` and use `Content-Type: application/json` | +| Upstream authentication, rate limit, or quota error | Check provider configuration, eligibility, and actual errors, following returned retry conditions | +| `502` / `504` or in-stream error | Check network, upstream response, and timeout; retain incomplete status instead of treating partial text as a complete result | +| Unregistered path | Use an endpoint listed here; valid credentials do not make an unknown path exist | + +The proxy accepts `x-api-key` or Bearer authentication. When both are sent, a nonempty `x-api-key` takes precedence. Error formats also differ by Messages, Chat Completions, upstream response, and streaming phase; inspect the actual body. + +Existing provider accounts, subscriptions, and cloud compute remain subject to their account, quota, and authorization conditions. A safe handle and protocol conversion grant no new provider eligibility. See [Compute Service Eligibility](../02-user-guide/10-settings/04-compute-service.md) and [Terms of Service](../terms.md). + +## Related Reference + +- [Connect to an Instance and Authenticate](./01-connection-and-auth.md): Agent Service addresses and credentials. +- [OpenAPI Downloads](./07-openapi.md): separate proxy specification and coverage. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/06-workflow-integration.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/06-workflow-integration.md new file mode 100644 index 00000000..32440fae --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/06-workflow-integration.md @@ -0,0 +1,192 @@ +--- +title: Workflow Integration +description: Use a separate Workflow Service address and access token to trigger administrator-preconfigured workflows and observe events and execution history. +slug: /api/workflow-integration +sidebar_position: 6 +keywords: [API, Workflow Service, workflow, Webhook, SSE, execution history, authorization] +--- + +# Workflow Integration + +Trusted deployment owners or administrator clients can trigger an existing workflow through **standalone Workflow Service**, then use its returned Run ID to observe events and read history. This page covers global workflows that the administrator has deployed, verified, and approved for the Webhook entry point. It grants no authority to execute arbitrary workflows or user resources. + +## Obtain the Separate Address and Credential {#workflow-connection} + +| Information | Where to obtain it | +|---|---| +| `WORKFLOW_URL` | Workflow Service address supplied by the administrator. Use its actual listener for local calls or a separately configured HTTPS address for remote calls | +| `WORKFLOW_SERVICE_TOKEN` | `WORKFLOW_SERVICE_TOKEN` in the standalone workflow process startup configuration; when not set separately, the deployment may use its configured `AGENT_SERVICE_TOKEN`, which the administrator must confirm | +| `WORKFLOW_ID` | An existing global workflow ID supplied by the administrator, verifiable through the list below; a display name cannot replace it | +| Parameter contract | Administrator-defined inputs, operations, and outcomes; first verify access with a test workflow that has no external side effects | + +The workflow service binds to loopback by default. Obtain its actual port from startup readiness information or the administrator. Its address and request header differ from Agent Service. Even if a deployment reuses the same token value, send `X-Workflow-Service-Token`. + +The default Agent Service container deployment provides no public workflow service entry point, and its reverse proxy rejects `/api/workflow/*`. Adding a workflow path to an Agent Service URL does not make it available. The administrator must supply a separately reachable Workflow Service address. + +For the standalone access described here, every business request below, including SSE, needs a valid `X-Workflow-Service-Token`. This is a service access credential, not separate execution authority for a user, task, or workflow. Run and workflow IDs do not replace credentials or resource permission checks. + +## Check the Workflow to Trigger {#list-workflows} + +### `GET /api/workflows` + +No request body or pagination parameters. Returns `{ "workflows": [...] }` with these item fields: + +| Field | Meaning | +|---|---| +| `id`, `name`, `description`, `creator` | Workflow ID, display name, description, and creator description | +| `nodeCount` | Number of nodes in the definition | +| `dslPath` | Service-managed definition path, not a file on the caller's computer or an access credential | +| `lastRun` | Optional compressed last-run summary with `status` and `taskBoardBound`, possibly completion time and duration | + +```bash +WORKFLOW_URL='https://workflow.example.com' +WORKFLOW_SERVICE_TOKEN='replace-with-the-configured-workflow-service-token' +WORKFLOW_ID='your-workflow-id' + +curl --silent --show-error \ + --header "X-Workflow-Service-Token: ${WORKFLOW_SERVICE_TOKEN}" \ + "${WORKFLOW_URL}/api/workflows" +``` + +Use discovery to check the definition supplied by the administrator. It does not authorize every workflow to run, and unparseable definitions may be absent from the list. + +## Trigger a Preconfigured Workflow {#trigger-workflow-webhook} + +### `POST /api/workflow/webhook?id=...` + +| Item | Contract | +|---|---| +| Query parameter `id` | Required ID of an existing global workflow | +| Headers | `X-Workflow-Service-Token` and `Content-Type: application/json` | +| Request body | This example uses a JSON object as workflow parameters; omission is treated as an empty object | +| Success | `200` with `runId`, `workflowId`, and `status: "started"` | + +This example assumes the administrator has defined a `name` input for the workflow. Replace fields according to its input contract: + +```bash +curl --silent --show-error --include --request POST \ + --header "X-Workflow-Service-Token: ${WORKFLOW_SERVICE_TOKEN}" \ + --header 'Content-Type: application/json' \ + --data '{"name":"API"}' \ + "${WORKFLOW_URL}/api/workflow/webhook?id=${WORKFLOW_ID}" +``` + +```json +{ + "runId": "00000000-0000-4000-8000-000000000010", + "workflowId": "your-workflow-id", + "status": "started" +} +``` + +Save the actual `runId`. `started` means the service received and scheduled the asynchronous execution. Definition parsing, nodes, permissions, or dependencies can still fail later; it does not mean completion. + +This workflow Webhook uses the service token and differs from the Bearer secret and event body used by [Agent Webhook](./02-agent-webhook.md). It has no request deduplication or public idempotency key. Putting a business event ID in parameters does not enable deduplication. A lost response may follow execution, so do not resend just to guess its outcome. + +## Observe Realtime Events {#workflow-events} + +### `GET /api/workflow/events?runId=...` + +Send the returned `runId` and service token to receive `text/event-stream`: + +```bash +WORKFLOW_RUN_ID='replace-with-the-returned-run-id' + +curl --no-buffer --silent --show-error --get \ + --header "X-Workflow-Service-Token: ${WORKFLOW_SERVICE_TOKEN}" \ + --data-urlencode "runId=${WORKFLOW_RUN_ID}" \ + "${WORKFLOW_URL}/api/workflow/events" +``` + +Each event has an `event:` name and JSON `data:`. Common events include: + +| Event | What to observe | +|---|---| +| `workflow:started` | Workflow and Run IDs, name, and total node count | +| `workflow:node_start` | Node ID, type, and optional display name | +| `workflow:node_complete` | Node inputs, outputs, and duration | +| `workflow:node_error` | Node ID and error; continue observing the whole-run outcome | +| `workflow:human_gate` | A human confirmation wait, not completion | +| `workflow:complete` | Run status and any final outputs; inspect `status` | +| `workflow:error` | Execution error, possibly with the failed node | + +Use one active subscription per Run. The stream grants access at the service credential boundary for trusted administrator clients. It does not implement arbitrary end-user resource access by Run ID. Native browser `EventSource` cannot directly set this custom authentication header; use a fetch stream that sends headers, and keep tokens out of URLs. + +Reconnecting to a Run that already ended in the current service process can return only one `workflow:complete`, whose `status` can also be `failed`. The event name alone does not establish success. Historical executions or realtime context lost after process restart can return `404`; use the history endpoints below. + +SSE does not durably replay every event. Disconnection, stream closure, or no events cannot independently establish success. Read state and history by the Run ID, and preserve unknown outcomes when unconfirmed. + +## Read Execution History {#workflow-history} + +### `GET /api/workflow/:id/runs` + +Returns `{ "runs": [...] }`, merging active and persisted executions ordered by `startedAt`, newest first. No pagination parameters. Example key fields of a summary item: + +```json +{ + "runs": [ + { + "runId": "00000000-0000-4000-8000-000000000010", + "status": "completed", + "startedAt": "2026-10-14T02:00:00.000Z", + "nodeCount": 2, + "completedNodeCount": 2, + "taskBoardBound": false + } + ] +} +``` + +Summaries can also include `completedAt`, `totalDurationMs`, `error`, and authorized task/execution ownership information. Active executions can temporarily have node counts of `0`, which does not mean their definitions contain no nodes. + +### `GET /api/workflow/:id/runs/:runId` + +Returns one execution detail object with `workflowId`, `runId`, `status`, `startedAt`, and `taskBoardBound`. Terminal executions can include `completedAt`, `totalDurationMs`, `nodeResults`, `finalOutputs`, and `error`; a human confirmation wait can include `humanGate`. + +```bash +curl --silent --show-error \ + --header "X-Workflow-Service-Token: ${WORKFLOW_SERVICE_TOKEN}" \ + "${WORKFLOW_URL}/api/workflow/${WORKFLOW_ID}/runs/${WORKFLOW_RUN_ID}" +``` + +`running` and `waiting_human` mean execution or pending confirmation. Terminal states are `completed`, `failed`, `cancelled`, and `interrupted`. Evaluate the actual status, error, and outputs. A Workflow `runId` belongs to Workflow Service and cannot be used with Agent Service `/api/runs/:id/receipt`. + +### TaskBoard-Associated Records + +History lists and details check permissions for associated TaskBoard resources; unreadable task records are not returned as visible results. Task-scoped queries use: + +| Query parameter | Condition | +|---|---| +| `boardId` | Required when specifying task scope | +| `workflowBindingId` | Required for task scope and must belong to this workflow and Board/Task | +| `taskId` | Optional; must be nonempty and match the binding when supplied | + +These parameters identify and filter resources; they grant no access. Invalid combinations or insufficient permissions can return `403`. Detail outside the requested task scope returns `404`. An empty list can also result from permission filtering or unreadable history, so it does not establish that nothing ran. + +## Manual Execution and Definition Write Scope {#workflow-supported-scope} + +`POST /api/workflow/run` requires a valid `executionGrantId`, `idempotencyKey`, and the corresponding definition reference or authorized path. A service access token does not replace execution authority. The trigger and read endpoints above do not issue it. Manual execution requires product-supported workflow operations or a separately supplied authorization integration; the quick start here uses a preconfigured Webhook. Do not invent a grant or copy an internal permission object. + +The old `POST /api/workflows` definition creation and `PUT /api/workflow/:id` replacement endpoints are retired and return `410`. Maintain definitions through the existing workflow management process. This page only triggers administrator-preconfigured workflows suitable for its Webhook. Use the corresponding product flow for human confirmation; receiving a `human_gate` message is not approval. + +## Errors and Recovery {#workflow-errors} + +| HTTP status or condition | Meaning and action | +|---|---| +| `401 unauthorized` | Missing or invalid workflow service token; check the dedicated header and address | +| `503 workflow_service_not_initialized` | Workflow access credential is uninitialized; ask the administrator to check startup configuration | +| `503 WORKFLOW_OWNER_FENCED` | The service cannot safely provide the operation; retain the response and ask the administrator to inspect service state, without unconditional trigger replay | +| Trigger `400 id query parameter is required` | Supply the workflow ID from the administrator | +| Trigger `404` | The global workflow is unreadable or missing; check the service and ID | +| Trigger `503 Unable to reserve workflow run` | Execution could not be reserved; inspect the actual response and service state | +| Events `400 runId is required` / `404 Run not found` | Missing Run ID or no realtime execution in this process; check the ID and read history | +| History `403` / `404` | Resource permissions, workflow/Run matching, or task scope do not allow the read; do not bypass ownership checks | +| Failure after successful trigger or SSE disconnection | Receipt is not completion; read history with the actual returned Run ID and retain unknown outcomes when unconfirmed | +| Request format, size, or dependency errors | Preserve the actual status and body, correct the request or investigate dependencies; errors do not share a universal envelope | + +Repeating reads updates observations; triggering again creates another execution. If the trigger response is missing, do not attribute the latest execution directly to this request. + +## Related Reference + +- [Task Orchestration](../04-concepts/10-task-orchestration.md): workflow usage and nodes. +- [OpenAPI Downloads](./07-openapi.md): parameters and response scope for these public operations. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/07-openapi.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/07-openapi.md new file mode 100644 index 00000000..2e32d6b9 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/07-openapi.md @@ -0,0 +1,88 @@ +--- +title: OpenAPI download +description: Download the OpenAPI definition for published DesireCore HTTP endpoints and select the right instance address and credential. +slug: /api/openapi +sidebar_position: 7 +--- + +# OpenAPI download + +Import the JSON file below into an API tool that supports [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0.html) to inspect parameters, responses, and authentication requirements. Request and response schemas declare the JSON Schema Draft 7 dialect. The file describes only the published HTTP endpoints listed in the table. + +**[Download Agent Service OpenAPI JSON](/openapi/agent-service.json)** + +Also available: **[model protocol proxy OpenAPI JSON](/openapi/compat-proxy.json)** and **[Workflow Service OpenAPI JSON](/openapi/workflow-service.json)**. Each file uses its own service address and credential. + +## Agent Service coverage + +| Method and path | Purpose | Guide | +|---|---|---| +| `GET /api/client-handshake` | Verify an existing instance credential and service identity | [Connection and authentication](./01-connection-and-auth.md) | +| `POST /api/webhook/{agent_id}` | Trigger an administrator-configured Agent event | [Agent Webhook](./02-agent-webhook.md) | +| `GET /api/agents/{id}/webhooks` | Read event settings and available Tools | [Agent Webhook](./02-agent-webhook.md) | +| `PUT /api/agents/{id}/webhooks/events/{event}` | Create or replace an event with selected Tools | [Agent Webhook](./02-agent-webhook.md) | +| `PATCH /api/agents/{id}/webhooks` | Enable or pause event triggers | [Agent Webhook](./02-agent-webhook.md) | +| `DELETE /api/agents/{id}/webhooks/events/{event}` | Delete an existing event | [Agent Webhook](./02-agent-webhook.md) | +| `POST /api/agents/{id}/webhooks/secret/rotate` | Rotate the Agent's dedicated Webhook secret | [Agent Webhook](./02-agent-webhook.md) | +| `GET /api/agents/{agentId}/schedule-messages` | Read visible background messages and deliveries | [Agent Webhook](./02-agent-webhook.md) | +| `GET /api/agents` | Read Agent summaries | [Agent list](./04-conversations-and-runs.md#list-agents) | +| `GET /api/runs` | Read filtered instance Run metadata | [Run list](./04-conversations-and-runs.md#list-runs) | +| `GET /api/runs/{id}` | Read one Run's metadata | [Run details](./04-conversations-and-runs.md#get-run) | +| `GET /api/runs/{id}/runtime-state` | Read the raw Run state | [Run state](./04-conversations-and-runs.md#get-run-runtime-state) | +| `GET /api/runs/{id}/receipt` | Read receipts owned by the trusted user | [Run receipts](./04-conversations-and-runs.md#list-run-receipts) | +| `POST /api/query/start` | Submit a new conversation request | [Conversations and Runs](./04-conversations-and-runs.md) | +| `POST /api/query/follow-up` | Submit another turn to an established session | [Conversations and Runs](./04-conversations-and-runs.md) | +| `POST /api/query/client-requests/lookup` | Reconcile admission by stable request ID | [Conversations and Runs](./04-conversations-and-runs.md) | + +Braces identify path parameters to replace, corresponding to colon notation in the endpoint guides. Webhook management requires `capabilities.webhookManagement: true` from the authenticated handshake and the instance owner's credential. Older instances without that capability use administrator-enabled and verified events. Run lists and details use the deployment owner's instance read access; receipt access additionally checks trusted-user ownership. + +## Model protocol proxy coverage + +| Method and path | Purpose | +|---|---| +| `GET /v1/models` | List models configured under enabled Providers | +| `POST /v1/messages` | Submit a Messages request, optionally with streaming SSE | +| `POST /v1/chat/completions` | Submit a Chat Completions request, optionally with streaming SSE | + +Follow [Model protocol proxy](./05-model-proxy.md) to obtain the current loopback Base URL and a Provider-bound safe token. That Base URL already includes `/v1`. Set the imported server to its origin, preserving the actual port, so the paths in the file add `/v1`. `ProxyBearer` uses Bearer and `ProxyApiKey` uses `x-api-key`; select either scheme for an operation. + +The model inventory is not filtered by the token's Provider binding. Listing a model does not authorize the token to call it. The file presents the published protocol subset; optional parameters, tools, and structured output still depend on conversion and the target backend. The client must inspect and handle errors within a stream. + +## Workflow Service coverage + +| Method and path | Purpose | +|---|---| +| `GET /api/workflows` | Read deployed Workflow metadata | +| `POST /api/workflow/webhook` | Trigger a preconfigured Workflow using required query parameter `id` | +| `GET /api/workflow/events` | Watch SSE events using required query parameter `runId` | +| `GET /api/workflow/{id}/runs` | Read visible Run summaries | +| `GET /api/workflow/{id}/runs/{runId}` | Read one Run's details | + +Use the Workflow Service address separately supplied by the administrator and the `WorkflowToken` scheme, which sends `X-Workflow-Service-Token`. The default Agent Service reverse proxy does not expose these Workflow paths. See [Workflow integration](./06-workflow-integration.md). + +The Workflow file presents response fields from the current implementation and permits additional fields. It does not claim that the HTTP interface applies closed Schema validation to every field. The preconfigured Webhook, event stream, and history reads do not issue `executionGrantId`. The file excludes `/api/workflow/run`, which requires additional execution authorization, and retired definition writes. + +## Set the address and credential after import + +Replace the example server `https://your-instance.example` with the instance HTTPS address supplied by the administrator. Preserve any nonstandard port. This address excludes `/api` and is separate from the documentation website address. See [Connection and authentication](./01-connection-and-auth.md). + +| Authentication scheme in the file | Credential | How to send it | +|---|---|---| +| `InstanceToken` | Existing instance access token | `X-Agent-Service-Token: ` | +| `WebhookSecret` | Dedicated secret for the target Agent Webhook | `Authorization: Bearer ` | + +The table applies to the Agent Service file. Select the scheme required by the operation. Provider API keys let DesireCore connect to model providers; the proxy and Workflow files use their own addresses and authentication schemes. + +## Interpret responses correctly + +A `200` handshake response verifies the current credential. Individual features also depend on instance configuration, permissions, and actual capabilities. + +Webhook `200` with `accepted: true` acknowledges the trigger. The response has no Run ID or final result, and a later background failure does not change the HTTP response. Follow the [Webhook guide](./02-agent-webhook.md) for result delivery, timeouts, and duplicate requests. + +Error structures vary by endpoint. Generated client code should preserve the HTTP status and actual response body. Reverse proxies may also return plain text or a webpage; see [Errors and troubleshooting](./03-errors-and-troubleshooting.md). + +Socket.IO connections, subscriptions, and asynchronous events require corresponding client logic. The OpenAPI file describes the HTTP portion and does not represent these events as SSE endpoints. + +A conversation submission's `202` response also does not prove completion. Connect Socket.IO first and submit its current `socket.id` as `originSocketId`; the HTTP request and socket must identify the same authenticated principal. Stable `clientInstanceId` belongs to Socket authentication data. After a disconnect, follow the reconciliation steps in [Conversations and Runs](./04-conversations-and-runs.md); a Run's `idle` state does not prove that a particular submission succeeded. + +The file's `info.version` is the version of this HTTP interface description. It is separate from DesireCore feature effective versions and the handshake's `serverVersion` and `apiProtocolVersion`. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/_category_.json b/i18n/en/docusaurus-plugin-content-docs/current/07-api/_category_.json new file mode 100644 index 00000000..11b05a22 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Integrations and API", + "position": 7, + "link": { + "type": "doc", + "id": "api/index" + } +} diff --git a/i18n/en/docusaurus-plugin-content-docs/current/07-api/index.md b/i18n/en/docusaurus-plugin-content-docs/current/07-api/index.md new file mode 100644 index 00000000..52e20510 --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/07-api/index.md @@ -0,0 +1,39 @@ +--- +title: Integration & API +description: Connect an instance, run conversations, configure Agent Webhooks, and use model and workflow services. +slug: /api +sidebar_position: 0 +keywords: [API, integration, Webhook, Agent Service, authentication] +--- + +# Integration & API + +Connect your scripts or services to DesireCore using the address and credentials for the service you need. The guides cover connection checks, conversations and results, Agent Webhooks, model protocols, and workflow integration. + +## Choose a guide + +| Goal | Requirements | Guide | +|---|---|---| +| Connect an instance | Administrator-provided HTTPS address and instance token | [Connection and authentication](./01-connection-and-auth.md) | +| Submit conversations and follow results | Instance owner access, Agent ID, Node.js 22+ for the example | [Conversations and Runs](./04-conversations-and-runs.md) | +| Trigger an Agent from external events | Enabled event and Agent-level Webhook secret; owner configuration requires the `webhookManagement` capability | [Agent Webhook](./02-agent-webhook.md) | +| Call model protocols through the local proxy | Running local proxy and provider-bound safe token | [Model proxy](./05-model-proxy.md) | +| Trigger an existing workflow | Separate Workflow Service address, service token, and administrator-provided definition | [Workflow integration](./06-workflow-integration.md) | +| Import HTTP contracts into a tool | Select the spec for your service | [OpenAPI downloads](./07-openapi.md) | +| Let DesireCore call your business service | MCP server, tools or skills | [MCP integration](../02-user-guide/09-capabilities/03-mcp-integration.md) and [integration overview](../02-user-guide/09-capabilities/06-integrations-overview.md) | + +## Credentials and results + +Agent Service uses `X-Agent-Service-Token`; external Webhooks use `Authorization: Bearer `. Model proxy credentials and Workflow Service tokens have their own scope. A provider API Key does not substitute for an instance credential. + +A handshake `200` confirms the authenticated request. Query submission `202` and Webhook `200 accepted` acknowledge admission or receipt; follow the corresponding guide to observe completion and delivery. Conversation events use Socket.IO, while Workflow progress uses SSE. + +## Start integrating + +1. Obtain the actual service address and its credential. Use verified HTTPS for remote access. +2. Complete the connection check or the selected guide's minimal example. +3. Check the actual response and result delivery. See [errors and troubleshooting](./03-errors-and-troubleshooting.md) for failures. + +:::tip Documentation and instance addresses +`https://docs.desirecore.com/api` is a documentation page. Send API requests to your own instance address, such as `https://agent.example.com`. +::: diff --git a/i18n/en/docusaurus-plugin-content-docs/current/index.md b/i18n/en/docusaurus-plugin-content-docs/current/index.md index 8fc6a030..c956314d 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/index.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/index.md @@ -76,6 +76,7 @@ Having issues? Find the most common answers here. ## Popular Topics +- [Integrations and API](./07-api/index.md) — Connect external programs to an instance and configured Webhooks - [Configure API Key](./getting-started/configure-api-key) — The first step to connect AI services - [Teach Your Agent](./user-guide/teaching/delegation-interaction) — DesireCore's core capability - [Enterprise Business Ontology](./concepts/business-ontology) — Connect objects, rules, actions, permissions, and FDE-style AI delivery diff --git a/i18n/en/docusaurus-theme-classic/footer.json b/i18n/en/docusaurus-theme-classic/footer.json index b4b13f9f..d64bdd38 100644 --- a/i18n/en/docusaurus-theme-classic/footer.json +++ b/i18n/en/docusaurus-theme-classic/footer.json @@ -58,5 +58,9 @@ "link.item.label.文档仓库": { "message": "Docs Repository", "description": "The label of footer link with label=文档仓库 linking to https://github.com/desirecore/docs" + }, + "link.item.label.集成与 API": { + "message": "Integrations and API", + "description": "The label of footer link with label=集成与 API linking to /api" } } diff --git a/i18n/en/docusaurus-theme-classic/navbar.json b/i18n/en/docusaurus-theme-classic/navbar.json index 09db8290..1a3ff5de 100644 --- a/i18n/en/docusaurus-theme-classic/navbar.json +++ b/i18n/en/docusaurus-theme-classic/navbar.json @@ -42,5 +42,9 @@ "item.label.GitHub": { "message": "GitHub", "description": "Navbar item with label GitHub" + }, + "item.label.API": { + "message": "API", + "description": "Navbar item with label API" } } diff --git a/package.json b/package.json index 7aebec06..8b0a785e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc", + "api:check": "node scripts/check-api-docs.mjs", "sitemap:check": "node scripts/check-sitemap-links.mjs", "changelog:cover": "node scripts/generate-changelog-cover.mjs" }, diff --git a/scripts/check-api-docs.mjs b/scripts/check-api-docs.mjs new file mode 100644 index 00000000..031ce70a --- /dev/null +++ b/scripts/check-api-docs.mjs @@ -0,0 +1,347 @@ +#!/usr/bin/env node + +// Offline checks for selected public API pages and OpenAPI downloads. Source provenance is maintenance +// data: this checker does not execute private source or certify runtime behavior. +import { readFile, access } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const args = process.argv.slice(2).filter(argument => argument !== '--') +const failures = [] +const fail = message => failures.push(message) +const isRecord = value => value !== null && typeof value === 'object' && !Array.isArray(value) +const isText = value => typeof value === 'string' && value.trim().length > 0 +const normalizeRoute = value => value === '/' ? '/' : value.replace(/\/$/, '') +const stringValue = value => value.trim().replace(/^(['"])(.*)\1$/, '$2') + +function localPath(relative, context) { + if (!isText(relative) || path.isAbsolute(relative) || relative.includes('\\')) { + fail(`${context}: expected a repository-relative path`) + return null + } + const resolved = path.resolve(root, relative) + if (!resolved.startsWith(root + path.sep)) { + fail(`${context}: path escapes the repository`) + return null + } + return resolved +} + +async function read(relative) { + const file = localPath(relative, relative) + if (!file) return null + try { + return await readFile(file, 'utf8') + } catch (error) { + fail(`${relative}: ${error.code ?? error.message}`) + return null + } +} + +async function readBytes(relative) { + const file = localPath(relative, relative) + if (!file) return null + try { return await readFile(file) } catch (error) { fail(`${relative}: ${error.code ?? error.message}`); return null } +} + +function scanMarkdown(content, file) { + const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] + const slug = frontmatter?.match(/^slug:\s*(.*?)\s*$/m)?.[1] + if (!slug) fail(`${file}: an explicit frontmatter slug is required`) + const versions = [] + let scope = 'page' + let headingOrdinal = 0 + let fence = null + let admonition = null + let jsonBlocks = 0 + const lines = content.split(/\r?\n/) + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index] + if (fence) { + const closing = line.match(/^ {0,3}(`{3,}|~{3,})\s*$/) + if (closing && closing[1][0] === fence.char && closing[1].length >= fence.length) { + if (fence.json) { + jsonBlocks += 1 + try { JSON.parse(fence.lines.join('\n')) } catch (error) { + fail(`${file}:${fence.start}: invalid JSON code block (${error.message})`) + } + } + fence = null + } else { + fence.lines.push(line) + } + continue + } + const opening = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/) + if (opening) { + fence = { char: opening[1][0], length: opening[1].length, json: /^json(?:\s|$)/i.test(opening[2].trim()), lines: [], start: index + 1 } + continue + } + const heading = line.match(/^(#{1,6})\s+(.+)$/) + if (heading) { + headingOrdinal += 1 + // Matching explicit heading IDs provide a stable bilingual scope key. + // Without IDs, matching heading positions only checks structural placement. + scope = heading[2].match(/\{#([^}]+)\}\s*$/)?.[1] ?? `heading-${headingOrdinal}-level-${heading[1].length}` + } + if (admonition) { + if (/^:::\s*$/.test(line)) { + const text = admonition.lines.join('\n') + const matches = [...text.matchAll(/`(v\d+\.\d+\.\d+\+)`/g)].map(match => match[1]) + if (matches.length !== 1) fail(`${file}:${admonition.start}: effective-version callout must contain exactly one inline vX.Y.Z+ version`) + if (!text.replace(/`v\d+\.\d+\.\d+\+`/g, '').replace(/[\s().。()]/g, '')) fail(`${file}:${admonition.start}: effective-version callout must identify its feature scope`) + versions.push({ version: matches[0], scope: admonition.scope }) + admonition = null + } else { + if (/^:::/.test(line)) fail(`${file}:${index + 1}: effective-version callout must not contain another admonition`) + admonition.lines.push(line) + } + } else if (/^:::info\s+(生效版本|Effective version)\s*$/.test(line)) { + const expected = file.startsWith('i18n/en/') ? 'Effective version' : '生效版本' + if (!line.trimEnd().endsWith(expected)) fail(`${file}:${index + 1}: expected callout title ${expected}`) + admonition = { start: index + 1, scope, lines: [] } + } + } + if (fence?.json) fail(`${file}:${fence.start}: unclosed JSON code block`) + if (admonition) fail(`${file}:${admonition.start}: unclosed effective-version callout`) + return { slug: slug ? stringValue(slug) : null, versions, jsonBlocks } +} + +function decodeXml(value) { + const entities = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" } + // Decode one source entity per match; replacement text must not be decoded again. + return value.replace(/&(amp|lt|gt|quot|apos);/g, (_, entity) => entities[entity]) +} + +function linkMetadata(html) { + return [...html.matchAll(/]*>/gi)].map(match => Object.fromEntries( + [...match[0].matchAll(/([\w-]+)\s*=\s*["']([^"']*)["']/g)].map(attribute => [attribute[1].toLowerCase(), decodeXml(attribute[2])]) + )) +} + +function urlPath(url, context) { + try { return normalizeRoute(new URL(url).pathname) } catch { fail(`${context}: invalid absolute URL ${url}`); return null } +} + +const httpMethods = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']) +const openapiPath = value => value.replace(/:([A-Za-z_][\w]*)/g, '{$1}') + +function resolveLocalReference(spec, reference, context) { + if (!reference.startsWith('#/')) { fail(`${context}: public specs must use self-contained local references`); return null } + let value = spec + for (const key of reference.slice(2).split('/').map(part => part.replaceAll('~1', '/').replaceAll('~0', '~'))) value = value?.[key] + if (value === undefined) fail(`${context}: unresolved reference ${reference}`) + return value +} + +function checkReferences(spec, value, context) { + if (Array.isArray(value)) { for (const item of value) checkReferences(spec, item, context); return } + if (!isRecord(value)) return + if (isText(value.$ref)) resolveLocalReference(spec, value.$ref, context) + for (const child of Object.values(value)) checkReferences(spec, child, context) +} + +async function checkSpecs(catalog, pages) { + const specFiles = [...new Set(catalog.endpoints.map(endpoint => endpoint.spec).filter(Boolean))] + let operationCount = 0 + for (const file of specFiles) { + if (!isText(file) || !/^static\/openapi\/[a-z0-9-]+\.json$/.test(file)) { fail('endpoint.spec: expected a static/openapi/.json path'); continue } + const raw = await read(file) + if (raw === null) continue + let spec + try { spec = JSON.parse(raw) } catch (error) { fail(`${file}: invalid JSON (${error.message})`); continue } + if (!isRecord(spec) || !/^3\.1\.\d+$/.test(spec.openapi) || !isRecord(spec.info) || !isText(spec.info.title) || !isText(spec.info.version) || !isRecord(spec.paths)) { fail(`${file}: expected an OpenAPI 3.1 document with info and paths`); continue } + checkReferences(spec, spec, file) + const selected = catalog.endpoints.filter(endpoint => endpoint.spec === file) + const operations = new Map() + for (const [route, item] of Object.entries(spec.paths)) { + if (!route.startsWith('/') || !isRecord(item)) { fail(`${file}: invalid path item ${route}`); continue } + for (const [method, operation] of Object.entries(item)) { + if (!httpMethods.has(method)) continue + operationCount += 1 + if (!isRecord(operation) || !isText(operation.operationId) || !isRecord(operation.responses) || !Object.keys(operation.responses).length) { fail(`${file}: ${method.toUpperCase()} ${route} needs operationId and responses`); continue } + if (operations.has(operation.operationId)) fail(`${file}: duplicate operationId ${operation.operationId}`) + operations.set(operation.operationId, { route, method, operation }) + const endpoint = selected.find(candidate => candidate.operationId === operation.operationId) + if (!endpoint) { fail(`${file}: operation ${operation.operationId} is outside the catalog's selected spec scope`); continue } + if (endpoint.method.toLowerCase() !== method || openapiPath(endpoint.path) !== route) fail(`${file}: ${operation.operationId} method/path differs from catalog`) + if (spec['x-service'] !== endpoint.service) fail(`${file}: x-service differs from ${endpoint.service}`) + if (spec['x-source']?.revision !== endpoint.source?.revision || spec['x-source']?.repository !== endpoint.source?.repository) fail(`${file}: ${operation.operationId} source provenance differs from catalog`) + const parameters = [...(item.parameters ?? []), ...(operation.parameters ?? [])].map(parameter => parameter.$ref ? resolveLocalReference(spec, parameter.$ref, file) : parameter).filter(Boolean) + for (const match of route.matchAll(/\{([^}]+)\}/g)) { + if (!parameters.some(parameter => parameter.in === 'path' && parameter.name === match[1] && parameter.required === true)) fail(`${file}: ${operation.operationId} is missing required path parameter ${match[1]}`) + } + // Catalog lists the credential alternatives available to this operation. + // Public examples use one scheme per alternative, never anonymous access. + const requirements = operation.security ?? spec.security + if (!Array.isArray(endpoint.securitySchemes) || !endpoint.securitySchemes.length || endpoint.securitySchemes.some(name => !isText(name))) { fail(`${endpoint.operationId}: spec coverage requires explicit securitySchemes in the catalog`); continue } + if (!Array.isArray(requirements) || !requirements.length || requirements.some(requirement => !isRecord(requirement) || Object.keys(requirement).length !== 1)) { fail(`${file}: ${operation.operationId} requires explicit non-anonymous credential alternatives`); continue } + const actual = requirements.map(requirement => Object.keys(requirement)[0]).sort() + const expected = [...endpoint.securitySchemes].sort() + if (JSON.stringify(actual) !== JSON.stringify(expected)) fail(`${file}: ${operation.operationId} security schemes differ from catalog`) + for (const requirement of requirements) { + for (const [name, scopes] of Object.entries(requirement)) { + const scheme = spec.components?.securitySchemes?.[name] + if (!scheme) fail(`${file}: undefined security scheme ${name}`) + if (!Array.isArray(scopes) || scopes.length) fail(`${file}: ${name} must not claim OAuth scopes for these credential schemes`) + if (name === 'InstanceToken' && (scheme?.type !== 'apiKey' || scheme.in !== 'header' || scheme.name !== 'X-Agent-Service-Token')) fail(`${file}: InstanceToken must use the X-Agent-Service-Token header`) + if (name === 'WebhookSecret' && (scheme?.type !== 'http' || scheme.scheme !== 'bearer')) fail(`${file}: WebhookSecret must use HTTP Bearer authentication`) + if (name === 'ProxyBearer' && (scheme?.type !== 'http' || scheme.scheme !== 'bearer')) fail(`${file}: ProxyBearer must use HTTP Bearer authentication`) + if (name === 'ProxyApiKey' && (scheme?.type !== 'apiKey' || scheme.in !== 'header' || scheme.name?.toLowerCase() !== 'x-api-key')) fail(`${file}: ProxyApiKey must use the x-api-key header`) + if (name === 'WorkflowToken' && (scheme?.type !== 'apiKey' || scheme.in !== 'header' || scheme.name?.toLowerCase() !== 'x-workflow-service-token')) fail(`${file}: WorkflowToken must use the X-Workflow-Service-Token header`) + } + } + } + } + for (const endpoint of selected) if (!operations.has(endpoint.operationId)) fail(`${file}: catalog operation ${endpoint.operationId} is missing from the spec`) + const downloadPage = [...pages.values()].find(page => page.slug === '/api/openapi') + const href = '/' + file.replace(/^static\//, '') + if (!downloadPage) fail(`${file}: the catalog must include the /api/openapi download page`) + else { + for (const [language, content] of [['ZH', downloadPage.zhContent], ['EN', downloadPage.enContent]]) { + const targets = [...(content ?? '').matchAll(/\]\(([^\s)]+)(?:\s+[^)]*)?\)/g)].map(match => match[1].replace(/^pathname:\/\//, '')) + if (!targets.includes(href)) fail(`${file}: ${language} download page lacks a link to ${href}`) + } + } + if (args.includes('--build')) { + for (const language of ['', 'en']) { + const built = await read(path.posix.join('build', language, file.replace(/^static\//, ''))) + if (built !== null && built !== raw) fail(`${file}: ${language || 'ZH'} built download is stale or different`) + } + } + } + return { files: specFiles.length, operations: operationCount } +} + +async function checkClientExampleMirror() { + const sourceFile = 'api/examples/agent-client/client.mjs' + const sourcePath = localPath(sourceFile, sourceFile) + // Earlier batches may not include a client. Once it exists its public copy + // must be byte-for-byte equal, including both locale builds when requested. + try { await access(sourcePath) } catch (error) { if (error.code !== 'ENOENT') fail(`${sourceFile}: ${error.message}`); return } + const source = await readBytes(sourceFile) + const downloadFile = 'static/api-examples/agent-client.mjs' + const download = await readBytes(downloadFile) + if (source !== null && download !== null && !source.equals(download)) fail(`${downloadFile}: differs from the maintained Node client example`) + if (args.includes('--build')) { + for (const language of ['', 'en']) { + const file = path.posix.join('build', language, 'api-examples/agent-client.mjs') + const built = await readBytes(file) + if (built !== null && source !== null && !source.equals(built)) fail(`${file}: Node client download is stale or different`) + } + } +} + +async function checkBuild(pages) { + const sitemapUrls = new Map() + for (const language of ['', 'en']) { + const sitemapFile = path.posix.join('build', language, 'sitemap.xml') + const xml = await read(sitemapFile) + if (xml !== null) { + const entries = [...xml.matchAll(/([^<]*)<\/loc>/g)].map(match => decodeXml(match[1])) + if (!entries.length) fail(`${sitemapFile}: no sitemap URLs`) + sitemapUrls.set(language, entries) + } + } + for (const page of pages) { + for (const language of ['', 'en']) { + const route = normalizeRoute(`/${language ? language + '/' : ''}${page.slug.replace(/^\//, '')}`) + const stem = path.posix.join('build', route.replace(/^\//, '')) + const candidates = [`${stem}.html`, path.posix.join(stem, 'index.html')] + let html = null + let htmlFile = null + for (const candidate of candidates) { + const resolved = localPath(candidate, candidate) + if (!resolved) continue + try { await access(resolved); html = await readFile(resolved, 'utf8'); htmlFile = candidate; break } catch (error) { + if (error.code !== 'ENOENT') fail(`${candidate}: ${error.message}`) + } + } + if (html === null) { fail(`${route}: built HTML missing (${candidates.join(' or ')})`); continue } + const links = linkMetadata(html) + const canonicals = links.filter(link => link.rel === 'canonical') + if (canonicals.length !== 1) fail(`${htmlFile}: expected exactly one canonical`) + const canonical = canonicals[0]?.href + if (canonical && urlPath(canonical, htmlFile) !== route) fail(`${htmlFile}: canonical route does not match ${route}`) + if (canonical && !sitemapUrls.get(language)?.includes(canonical)) fail(`${htmlFile}: canonical absent from ${language || 'ZH'} sitemap`) + for (const [code, expected] of [['zh-Hans', normalizeRoute(page.slug)], ['en', normalizeRoute('/en' + page.slug)]]) { + const alternates = links.filter(link => link.rel === 'alternate' && link.hreflang === code) + if (alternates.length !== 1 || urlPath(alternates[0]?.href, htmlFile) !== expected) fail(`${htmlFile}: missing or incorrect ${code} alternate for ${expected}`) + } + } + } +} + +async function main() { + if (args.some(argument => !['--build', '--help'].includes(argument))) throw new Error('Usage: node scripts/check-api-docs.mjs [--build]') + if (args.includes('--help')) { console.log('Usage: node scripts/check-api-docs.mjs [--build]\nChecks catalog-selected bilingual API pages, self-contained OpenAPI scope, authentication and download links offline. --build also checks local HTML, canonicals, language alternates, sitemaps and copied downloads.'); return } + const raw = await read('api/endpoint-catalog.json') + if (raw === null) throw new Error(failures.join('\n')) + const catalog = JSON.parse(raw) + if (!isRecord(catalog) || catalog.schemaVersion !== 1 || !Array.isArray(catalog.pages) || !Array.isArray(catalog.endpoints)) throw new Error('Catalog must have schemaVersion: 1, pages: [], endpoints: []') + const pages = new Map() + const files = new Set() + const slugs = new Set() + let jsonBlocks = 0 + for (const page of catalog.pages) { + if (!isRecord(page) || !isText(page.id) || !isText(page.file) || !isText(page.slug)) { fail('pages[]: id, file and slug are required'); continue } + if (pages.has(page.id)) fail(`Duplicate page id ${page.id}`) + if (files.has(page.file)) fail(`Duplicate page file ${page.file}`) + if (slugs.has(page.slug)) fail(`Duplicate page slug ${page.slug}`) + files.add(page.file) + slugs.add(page.slug) + if (!/^docs\/07-api\/[^\n]+\.(md|mdx)$/.test(page.file)) fail(`${page.id}: page file must be under docs/07-api/`) + if (!/^\/api(?:\/[a-z0-9-]+)*$/.test(page.slug)) fail(`${page.id}: expected stable /api slug`) + const english = 'i18n/en/docusaurus-plugin-content-docs/current/' + page.file.replace(/^docs\//, '') + const zhContent = await read(page.file) + const enContent = await read(english) + const zh = zhContent === null ? null : scanMarkdown(zhContent, page.file) + const en = enContent === null ? null : scanMarkdown(enContent, english) + for (const [file, parsed] of [[page.file, zh], [english, en]]) { + if (parsed && parsed.slug !== page.slug) fail(`${file}: slug ${parsed.slug} differs from catalog ${page.slug}`) + jsonBlocks += parsed?.jsonBlocks ?? 0 + } + if (zh && en && JSON.stringify(zh.versions) !== JSON.stringify(en.versions)) fail(`${page.id}: bilingual version callout versions or heading scopes differ`) + pages.set(page.id, { ...page, zh, en, zhContent, enContent }) + } + const operationIds = new Set() + const routes = new Set() + for (const endpoint of catalog.endpoints) { + if (!isRecord(endpoint)) { fail('endpoints[]: expected an object'); continue } + const label = endpoint.operationId ?? '(missing operationId)' + for (const field of ['operationId', 'service', 'method', 'path', 'doc']) if (!isText(endpoint[field])) fail(`${label}: missing ${field}`) + if (operationIds.has(endpoint.operationId)) fail(`Duplicate operationId ${label}`) + operationIds.add(endpoint.operationId) + if (!/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$/.test(endpoint.method)) fail(`${label}: invalid HTTP method`) + if (!isText(endpoint.path) || !endpoint.path.startsWith('/') || /[?#\s]/.test(endpoint.path)) fail(`${label}: invalid endpoint path`) + // Each service has its own listener; equal paths across services are distinct. + const route = `${endpoint.service} ${endpoint.method} ${String(endpoint.path).replace(/:[A-Za-z_][\w]*|\{[^}]+\}/g, ':param')}` + if (routes.has(route)) fail(`Duplicate service/method/path ${route}`) + routes.add(route) + const page = pages.get(endpoint.doc) ?? [...pages.values()].find(candidate => candidate.file === endpoint.doc) + if (!page) fail(`${label}: doc must refer to a catalog page id or file`) + for (const field of ['deploymentModes', 'authentication', 'permissions', 'resultDelivery', 'schemaRefs', 'effectiveVersion', 'versionEvidence', 'verification', 'spec']) if (!Object.hasOwn(endpoint, field)) fail(`${label}: missing maintenance field ${field}`) + if (!Array.isArray(endpoint.deploymentModes) || !endpoint.deploymentModes.length || endpoint.deploymentModes.some(mode => !isText(mode))) fail(`${label}: deploymentModes must be a nonempty string array`) + if (!isRecord(endpoint.source) || !isText(endpoint.source.repository) || !/^[a-f0-9]{40}$/i.test(endpoint.source.revision) || !Array.isArray(endpoint.source.files) || !endpoint.source.files.length) fail(`${label}: source needs repository, fixed 40-character revision and files`) + if (endpoint.effectiveVersion) { + if (!/^v\d+\.\d+\.\d+\+$/.test(endpoint.effectiveVersion)) fail(`${label}: invalid effectiveVersion`) + if (!endpoint.versionEvidence) fail(`${label}: effectiveVersion needs versionEvidence`) + for (const [language, parsed] of [['ZH', page?.zh], ['EN', page?.en]]) if (parsed && !parsed.versions.some(callout => callout.version === endpoint.effectiveVersion)) fail(`${label}: ${language} page lacks effectiveVersion ${endpoint.effectiveVersion}`) + } + // Runtime/schema validation records are reviewed separately; no source grep here. + } + const specs = await checkSpecs(catalog, pages) + await checkClientExampleMirror() + if (args.includes('--build')) await checkBuild([...pages.values()]) + if (failures.length) { + console.error(`API documentation checks failed (${failures.length}):`) + for (const failure of failures) console.error(`- ${failure}`) + process.exitCode = 1 + } else { + console.log(`API documentation checks passed: ${pages.size} bilingual pages, ${catalog.endpoints.length} endpoints, ${jsonBlocks} JSON blocks, ${specs.files} specs / ${specs.operations} operations${args.includes('--build') ? ', local built routes and metadata' : ''}.`) + console.log('Version scope checks compare section placement; translated feature meaning and source/runtime claims still require review.') + } +} + +main().catch(error => { console.error(error.message); process.exitCode = 1 }) diff --git a/sidebars.ts b/sidebars.ts index 25b2df11..b03646db 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -27,6 +27,9 @@ const sidebars: SidebarsConfig = { faq: [ {type: 'autogenerated', dirName: '06-faq'}, ], + api: [ + {type: 'autogenerated', dirName: '07-api'}, + ], }; export default sidebars; diff --git a/static/api-examples/agent-client.mjs b/static/api-examples/agent-client.mjs new file mode 100644 index 00000000..514f8010 --- /dev/null +++ b/static/api-examples/agent-client.mjs @@ -0,0 +1,366 @@ +#!/usr/bin/env node +// DesireCore HTTP + Socket.IO example. No browser renderer or private IPC is required. +import { io } from 'socket.io-client'; +import { randomUUID } from 'node:crypto'; +import { readFileSync, writeFileSync, renameSync, existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { setTimeout as delay } from 'node:timers/promises'; + +export function queryDigest(prompt, userMessageId) { + // packages/shared/src/query/request-digest.ts: UTF-8 FNV-1a, not a security hash. + const canonical = JSON.stringify(['qrd1', prompt, userMessageId, [], []]); + let hash = 0xcbf29ce484222325n; + for (const byte of new TextEncoder().encode(canonical)) { + hash = ((hash ^ BigInt(byte)) * 0x100000001b3n) & 0xffffffffffffffffn; + } + return `qrd1:${hash.toString(16).padStart(16, '0')}`; +} + +const help = `Usage: + node client.mjs --prompt "Hello" [--follow-up "And ..."] --state ./request.json + node client.mjs --resume ./request.json + node client.mjs --recover ./request.json # read-only; does not resend + node client.mjs --cancel ./request.json # explicit cancellation request + +Environment: DESIRECORE_URL, DESIRECORE_TOKEN, DESIRECORE_AGENT_ID (new requests). +Options: --timeout-ms 300000, --http-timeout-ms 10000, --poll-ms 1000. +Use HTTPS remotely; HTTP is accepted only for localhost/127.0.0.1/::1. +Exit: 0 observed completed turn; 1 rejected/failed/interrupted; 2 unknown or recovery-only. +State files contain prompts and IDs, never the access token. Do not share them. +Ctrl+C exits this client without cancelling the server; use --cancel to request cancellation. +`; + +function argumentsFor(argv) { + const options = { followUps: [], timeoutMs: 300_000, httpTimeoutMs: 10_000, pollMs: 1_000 }; + const names = new Map([ + ['--prompt', 'prompt'], ['--state', 'state'], ['--resume', 'resume'], + ['--recover', 'recover'], ['--cancel', 'cancel'], ['--timeout-ms', 'timeoutMs'], + ['--http-timeout-ms', 'httpTimeoutMs'], ['--poll-ms', 'pollMs'], + ]); + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--help' || argv[i] === '-h') return { help: true }; + const option = argv[i]; + const value = argv[++i]; + if (!value || value.startsWith('--')) throw new Error(`Missing value for ${option}`); + if (option === '--follow-up') options.followUps.push(value); + else if (names.has(option)) options[names.get(option)] = value; + else throw new Error(`Unknown option ${option}`); + } + for (const key of ['timeoutMs', 'httpTimeoutMs', 'pollMs']) { + options[key] = Number(options[key]); + if (!Number.isSafeInteger(options[key]) || options[key] < 10) throw new Error(`Invalid ${key}`); + } + const modes = ['resume', 'recover', 'cancel'].filter(key => options[key]); + if (modes.length > 1 || (modes.length && (options.prompt || options.state || options.followUps.length))) { + throw new Error('Use either a new --prompt/--state or one of --resume/--recover/--cancel.'); + } + if (!modes.length && (!options.prompt?.trim() || !options.state)) throw new Error(help); + return options; +} + +function baseUrl(value) { + const url = new URL(value); + if (url.username || url.password || url.search || url.hash || url.pathname !== '/') { + throw new Error('DESIRECORE_URL must be an origin without credentials, a path, query, or fragment.'); + } + const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname); + if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) { + throw new Error('Use HTTPS for remote instances.'); + } + return url.origin; +} + +function newTurn(prompt, kind, sessionId, agentId, conversationId) { + const clientRequestId = randomUUID(); + const userMessageId = randomUUID(); + const requestDigest = queryDigest(prompt, userMessageId); + const body = kind === 'start' + ? { prompt, options: { agentId, conversationId }, clientRequestId, userMessageId, requestDigest } + : { sessionId, message: { id: userMessageId, role: 'user', content: prompt, timestamp: Date.now() }, clientRequestId, requestDigest }; + return { kind, body, userMessageId, firstDispatchedAt: null, firstDispatchedServerTime: null, attempts: 0 }; +} + +function readState(file) { + const state = JSON.parse(readFileSync(file, 'utf8')); + if (state.version !== 1 || !state.agentId || !state.instanceId || !Array.isArray(state.turns) + || !state.turns.length || !Array.isArray(state.pendingFollowUps) + || !/^[A-Za-z0-9._:-]{16,128}$/.test(state.clientInstanceId)) throw new Error('Invalid state file'); + state.baseUrl = baseUrl(state.baseUrl); + for (const turn of state.turns) { + const prompt = turn.kind === 'start' ? turn.body?.prompt : turn.body?.message?.content; + if (!['start', 'follow-up'].includes(turn.kind) || typeof prompt !== 'string' + || !turn.body?.clientRequestId || !turn.userMessageId + || turn.body.requestDigest !== queryDigest(prompt, turn.userMessageId)) { + throw new Error('Invalid or edited request state. Create a new state file for a different request.'); + } + } + return state; +} + +export async function runClient(options, env = process.env, output = {}) { + const log = output.log ?? (data => process.stderr.write(`${JSON.stringify(data)}\n`)); + const text = output.text ?? (value => process.stdout.write(value)); + const filename = resolve(options.resume || options.recover || options.cancel || options.state); + const existing = Boolean(options.resume || options.recover || options.cancel); + if (!existing && existsSync(filename)) throw new Error('State file already exists. Use --resume or a new --state path.'); + const token = env.DESIRECORE_TOKEN; + if (!token) throw new Error('Set DESIRECORE_TOKEN to the existing instance access token.'); + const origin = existing ? readState(filename).baseUrl : baseUrl(env.DESIRECORE_URL); + if (existing && env.DESIRECORE_URL && baseUrl(env.DESIRECORE_URL) !== origin) { + throw new Error('DESIRECORE_URL differs from the saved request; use the original instance.'); + } + const timeoutMs = options.timeoutMs ?? 300_000; + const httpTimeoutMs = options.httpTimeoutMs ?? 10_000; + const pollMs = options.pollMs ?? 1_000; + async function http(path, body) { + const response = await fetch(`${origin}${path}`, { + method: body === undefined ? 'GET' : 'POST', + redirect: 'error', signal: AbortSignal.timeout(httpTimeoutMs), + headers: { 'X-Agent-Service-Token': token, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }) }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + if (!response.headers.get('content-type')?.includes('application/json')) { + throw new Error(`HTTP ${response.status}: expected JSON from the instance API`); + } + return { status: response.status, data: await response.json(), retryAfter: response.headers.get('retry-after') }; + } + async function handshake() { + const deadline = Date.now() + httpTimeoutMs; + while (true) { + const result = await http('/api/client-handshake'); + if (result.status === 503 && result.data?.code === 'service_not_ready' && Date.now() < deadline) { + await delay(Math.max(1_000, Number(result.retryAfter || 1) * 1_000)); + continue; + } + if (result.status !== 200 || result.data.service !== 'desirecore-agent-service' + || !result.data.instanceId || result.data.apiProtocolVersion !== '1') { + throw new Error(`Handshake rejected: HTTP ${result.status} ${result.data.error || result.data.code || 'unsupported protocol'}`); + } + return result.data; + } + } + const metadata = await handshake(); + const clock = { local: Date.now(), server: Date.parse(metadata.serverTime) }; + if (!Number.isFinite(clock.server)) throw new Error('Handshake serverTime is invalid.'); + const state = existing ? readState(filename) : { + version: 1, baseUrl: origin, instanceId: metadata.instanceId, agentId: env.DESIRECORE_AGENT_ID, + conversationId: env.DESIRECORE_AGENT_ID, clientInstanceId: randomUUID(), + turns: [], pendingFollowUps: [...(options.followUps ?? [])], + }; + if (!state.agentId) throw new Error('Set DESIRECORE_AGENT_ID to the target Agent ID.'); + if (state.instanceId !== metadata.instanceId) throw new Error('Instance identity changed; refusing to send or recover requests on another instance.'); + if (!existing) state.turns.push(newTurn(options.prompt, 'start', undefined, state.agentId, state.conversationId)); + function save() { + const temporary = `${filename}.${process.pid}.tmp`; + writeFileSync(temporary, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 }); + renameSync(temporary, filename); + } + save(); + log({ event: 'handshake', instanceId: metadata.instanceId, serverVersion: metadata.serverVersion, state: filename }); + + async function lookup(turn) { + const result = await http('/api/query/client-requests/lookup', { + agentId: state.agentId, conversationId: state.conversationId, + requests: [{ clientRequestId: turn.body.clientRequestId, userMessageId: turn.userMessageId }], + }); + if (result.status !== 200) throw new Error(`Admission lookup: HTTP ${result.status} ${result.data.error || result.data.code || ''}`); + const verdict = result.data.results?.find(item => item.clientRequestId === turn.body.clientRequestId); + if (result.data.protocolVersion !== 1 || !['committed', 'reserved', 'absent', 'unknown'].includes(verdict?.status)) { + throw new Error('Unsupported admission lookup response'); + } + turn.admission = { ...verdict, serverInstanceId: result.data.serverInstanceId, observedAt: result.data.observedAt }; + if (verdict.sessionId) turn.sessionId = verdict.sessionId; + save(); + return { verdict, envelope: result.data }; + } + async function recovery(turn) { + const admission = await lookup(turn); + if (!turn.runId && turn.sessionId) { + const listing = await http(`/api/runs?agent_id=${encodeURIComponent(state.agentId)}&limit=100`); + const exact = listing.data.runs?.filter(run => run.agent_id === state.agentId && run.session_id === turn.sessionId); + if (listing.status === 200 && exact?.length === 1) turn.runId = exact[0].run_id; + } + const snapshot = { admission: admission.verdict, serverInstanceId: admission.envelope.serverInstanceId }; + if (turn.runId) { + const path = `/api/runs/${encodeURIComponent(turn.runId)}`; + const runtime = await http(`${path}/runtime-state`); + const history = await http(`${path}/receipt?offset=0&limit=10`); + snapshot.runtime = { httpStatus: runtime.status, ...runtime.data }; + snapshot.receipts = { httpStatus: history.status, ...history.data }; + } + turn.recovery = snapshot; + save(); + log({ event: 'recovery', clientRequestId: turn.body.clientRequestId, ...snapshot, + note: 'Run status and receipts are snapshots; they do not prove this request completed. Missing Socket events are not replayed.' }); + return admission; + } + let turn = state.turns.at(-1); + if (options.recover) { await recovery(turn); return 2; } + + const socket = io(origin, { + path: '/socket.io/', auth: { token, clientInstanceId: state.clientInstanceId }, + autoConnect: false, forceNew: true, reconnection: true, + }); + let disconnected = false; + let fatalSocketError; + socket.on('disconnect', reason => { disconnected = true; log({ event: 'disconnected', reason }); }); + socket.on('connect_error', error => { + log({ event: 'connect_error', message: error.message }); + if (['unauthorized', 'instance_token_uninitialized'].includes(error.message)) fatalSocketError = error; + }); + socket.on('connect', () => log({ event: 'connected', socketId: socket.id, clientInstanceId: state.clientInstanceId })); + function matching(data) { return state.turns.find(item => item.body.clientRequestId === data?.clientRequestId); } + function bind(data, item) { + if (data.sessionId) item.sessionId = data.sessionId; + if (data.runId || data.terminal?.runId) item.runId = data.runId || data.terminal.runId; + } + for (const name of ['query:started', 'query:followUpAccepted']) socket.on(name, data => { + const item = matching(data); + if (!item) return; + bind(data, item); item.acceptedEvent = name; save(); + log({ event: name, clientRequestId: data.clientRequestId, sessionId: item.sessionId, runId: item.runId }); + }); + socket.on('query:message', data => { + const item = matching(data); + if (!item || item !== turn || item.outcome) return; + bind(data, item); + const message = data.message; + const delta = message?.type === 'stream_event' ? message.event?.delta : undefined; + if (message?.event?.type === 'content_block_delta' && delta?.type === 'text_delta' && typeof delta.text === 'string') { + item.sawTextDelta = true; text(delta.text); + } else if (!item.sawTextDelta && message?.type === 'text' && typeof message.text === 'string') text(message.text); + else if (!item.sawTextDelta && message?.type === 'assistant') { + for (const block of message.message?.content ?? []) if (block.type === 'text' && typeof block.text === 'string') text(block.text); + } + // result/completed_idle/user:message_delivery and approvals are not terminal events. + }); + socket.on('query:complete', data => { + const item = matching(data); + if (!item || item.outcome) return; + bind(data, item); + item.outcome = { status: data.terminal?.status === 'completed' ? 'completed' + : data.terminal?.status === 'interrupted' ? 'interrupted' : 'unknown', + source: 'query:complete', reason: data.terminal?.reason, partial: data.terminal?.partial }; + save(); text('\n'); log({ event: 'turn_terminal', clientRequestId: data.clientRequestId, ...item.outcome }); + }); + socket.on('query:error', data => { + const item = matching(data); + if (!item || item.outcome) return; + bind(data, item); item.outcome = { status: 'failed', source: 'query:error', code: data.code, error: data.error }; + save(); log({ event: 'turn_terminal', clientRequestId: data.clientRequestId, ...item.outcome }); + }); + socket.on('activity:run_status', data => { + if (turn.runId === data.runId) log({ event: 'run_status', runId: data.runId, status: data.status, + note: 'Run state does not identify completion of this turn.' }); + }); + async function connected(deadline) { + while (!socket.connected) { + if (fatalSocketError) throw fatalSocketError; + if (Date.now() >= deadline) throw new Error('Socket connection timed out; saved request has not been automatically cancelled.'); + await delay(Math.min(pollMs, 100)); + } + } + async function dispatch(item) { + if (item.firstDispatchedAt === null) { + item.firstDispatchedAt = Date.now(); + item.firstDispatchedServerTime = clock.server + item.firstDispatchedAt - clock.local; + } + item.attempts++; save(); // Write request identity before performing the POST. + let response; + try { response = await http(`/api/query/${item.kind}`, { ...item.body, originSocketId: socket.id }); } + catch (error) { + log({ event: 'admission_unknown', clientRequestId: item.body.clientRequestId, message: error.message }); + return; + } + if (response.status === 202 && response.data.ok === true + && response.data.clientRequestId === item.body.clientRequestId) { + if (response.data.sessionId) item.sessionId = response.data.sessionId; + item.httpAccepted = true; save(); + log({ event: 'http_admission', status: 202, duplicate: response.data.duplicate === true, + clientRequestId: item.body.clientRequestId, note: 'Admission, not completion or durable commitment.' }); + } else if (response.status === 202) throw new Error('Admission response did not echo the request identity.'); + else if ([400, 401, 403, 404, 409, 413, 415].includes(response.status) && !response.data.retryable) { + item.outcome ??= { status: 'rejected', source: 'http', httpStatus: response.status, + code: response.data.code, error: response.data.error }; + save(); log({ event: 'turn_rejected', clientRequestId: item.body.clientRequestId, ...item.outcome }); + } else log({ event: 'admission_pending', status: response.status, code: response.data.code, + busy: response.data.busy === true, clientRequestId: item.body.clientRequestId }); + } + function replayAllowed(item, envelope) { + const age = Date.parse(envelope.observedAt) - item.firstDispatchedServerTime; + const window = Math.min(envelope.autoReplayRetentionMs, envelope.tombstoneRetentionMs); + return Number.isFinite(age) && age >= 0 && window > 0 && age < window; + } + socket.connect(); + try { + while (true) { + const deadline = Date.now() + timeoutMs; + await connected(deadline); + if (options.cancel) { + if (turn.outcome) { log({ event: 'already_terminal', outcome: turn.outcome }); return turn.outcome.status === 'completed' ? 0 : 1; } + socket.emit('query:abort', { ...(turn.sessionId ? { sessionId: turn.sessionId } : {}), + agentId: state.agentId, conversationId: state.conversationId, clientRequestId: turn.body.clientRequestId }); + log({ event: 'cancel_requested', clientRequestId: turn.body.clientRequestId }); + } else if (!turn.outcome && turn.firstDispatchedAt === null) await dispatch(turn); + + while (!turn.outcome && Date.now() < deadline) { + await delay(pollMs); + if (turn.outcome) break; + if (fatalSocketError) throw fatalSocketError; + if (!socket.connected) continue; + try { + const observation = disconnected ? await recovery(turn) : await lookup(turn); + disconnected = false; + if (turn.outcome) break; + const { verdict, envelope } = observation; + if (verdict.status === 'unknown') { + log({ event: 'outcome_unknown', clientRequestId: turn.body.clientRequestId, note: 'No automatic resend.' }); + return 2; + } + if (verdict.status === 'committed' && verdict.committedServerInstance === 'previous') { + await recovery(turn); return 2; + } + if (verdict.status === 'absent' && !options.cancel) { + if (!replayAllowed(turn, envelope)) { + log({ event: 'replay_window_expired', clientRequestId: turn.body.clientRequestId }); return 2; + } + await dispatch(turn); // Preserve IDs, prompt, digest, and message; replace only originSocketId. + } + // committed/reserved wait for this turn's events. Neither proves business completion. + } catch (error) { log({ event: 'reconciliation_unavailable', message: error.message }); } + } + if (!turn.outcome) { + try { await recovery(turn); } catch (error) { log({ event: 'reconciliation_unavailable', message: error.message }); } + log({ event: 'outcome_unknown', clientRequestId: turn.body.clientRequestId, note: 'Timed out locally; server may still run. Use --recover or --resume.' }); + return 2; + } + if (turn.outcome.status !== 'completed') return turn.outcome.status === 'unknown' ? 2 : 1; + if (!state.pendingFollowUps.length || options.cancel) return 0; + // Follow-ups reuse a live session; never silently create a successor Run after a restart. + const observed = await lookup(turn); + if (!turn.sessionId || observed.verdict.status !== 'committed' + || observed.verdict.committedServerInstance !== 'current') { + await recovery(turn); log({ event: 'follow_up_unavailable', note: 'A live current-process session was not confirmed.' }); return 2; + } + const next = state.pendingFollowUps.shift(); + turn = newTurn(next, 'follow-up', turn.sessionId, state.agentId, state.conversationId); + state.turns.push(turn); save(); + } + } finally { socket.disconnect(); save(); } +} + +export async function main(argv = process.argv.slice(2)) { + try { + const options = argumentsFor(argv); + if (options.help) { process.stdout.write(help); return 0; } + return await runClient(options); + } catch (error) { + process.stderr.write(`${JSON.stringify({ event: 'client_error', message: error.message })}\n`); + return 2; + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + process.exitCode = await main(); +} diff --git a/static/openapi/agent-service.json b/static/openapi/agent-service.json new file mode 100644 index 00000000..6aed431a --- /dev/null +++ b/static/openapi/agent-service.json @@ -0,0 +1,6607 @@ +{ + "openapi": "3.1.0", + "jsonSchemaDialect": "http://json-schema.org/draft-07/schema#", + "info": { + "title": "DesireCore Agent Service — public HTTP subset", + "version": "1.0.0", + "description": "Selected externally callable endpoints for an administrator-provided standalone instance. This document is a subset of the service, and is not an application release declaration. Consult the linked guides for deployment, authorization and asynchronous results." + }, + "servers": [ + { + "url": "https://{instanceHost}", + "description": "Replace with the HTTPS address supplied by the administrator. This is not the public documentation site.", + "variables": { + "instanceHost": { + "default": "your-instance.example", + "description": "Actual instance host, including a nonstandard port when required." + } + } + } + ], + "tags": [ + { + "name": "Connection", + "description": "Instance identity and authenticated connection checks." + }, + { + "name": "Webhook", + "description": "Administrator-configured external event triggers." + }, + { + "name": "Read-only", + "description": "Owner read-only metadata and user-owned receipts." + }, + { + "name": "Conversations", + "description": "HTTP submission and admission lookup; asynchronous messages use Socket.IO." + }, + { + "name": "Webhook management", + "description": "Owner configuration, selected Tool grants and dedicated secret rotation." + } + ], + "paths": { + "/api/client-handshake": { + "get": { + "operationId": "getAgentServiceClientHandshake", + "tags": [ + "Connection" + ], + "summary": "Verify an existing instance credential and identify the service", + "description": "Does not issue credentials. A 200 response does not establish that every optional capability is configured. Extension headers only request metadata; they do not grant permissions.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/connection-and-auth" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "x-desirecore-handshake-extensions", + "in": "header", + "required": false, + "description": "Optional comma-separated extensions: product-identity, query-dispatch, deployment-capabilities. Unrecognized names are ignored.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Authenticated service identity and capability metadata.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientHandshake" + }, + "example": { + "service": "desirecore-agent-service", + "apiProtocolVersion": "1", + "serverVersion": "10.0.180", + "instanceId": "inst_0123456789abcdefghijklmnopqrstuv", + "deploymentMode": "external", + "clientCompatibility": { + "minimumVersion": "0.0.0", + "maximumVersion": null + }, + "capabilities": { + "workflow": false, + "mediaSignedUrl": true, + "pdfRendering": false, + "localPairing": true, + "queryRequestIdEcho": true + }, + "authentication": { + "kind": "instance-token", + "scopes": [ + "owner" + ] + }, + "serverTime": "2026-10-14T08:00:00.000Z" + } + } + }, + "headers": { + "Cache-Control": { + "schema": { + "type": "string", + "const": "no-store, private" + } + }, + "Vary": { + "schema": { + "type": "string" + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "A browser Origin is not allowed by this deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OriginRejection" + } + } + } + }, + "500": { + "description": "Service identity cannot be read or another server failure occurred; the response does not expose the data-directory path.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FrameworkError" + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/webhook/{agent_id}": { + "post": { + "operationId": "triggerAgentWebhook", + "tags": [ + "Webhook" + ], + "summary": "Trigger an administrator-configured Agent event", + "description": "The Agent and event must already be enabled and configured. Receives an event and starts background processing. A 200 acknowledgement contains no Run ID or final result. There is no public idempotency key; repeated requests may trigger repeated executions.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/agent-webhook" + }, + "security": [ + { + "WebhookSecret": [] + } + ], + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "description": "The administrator-provided Agent ID, not its display name.", + "schema": { + "type": "string" + } + } + ], + "x-max-body-bytes": 1048576, + "requestBody": { + "required": true, + "description": "The complete JSON body must be at most 1 MiB. Only event and payload are accepted at the top level.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookRequest" + }, + "example": { + "event": "demo.ping", + "payload": { + "message": "Test event: please confirm receipt." + } + } + } + } + }, + "responses": { + "200": { + "description": "The event was accepted; background execution may subsequently fail.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookAccepted" + }, + "example": { + "accepted": true, + "agentId": "your-agent-id", + "event": "demo.ping" + } + } + } + }, + "400": { + "description": "Request Schema, unknown event, or JSON parsing error.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebhookInvalidBody" + }, + { + "$ref": "#/components/schemas/WebhookUnknownEvent" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "401": { + "description": "The configured dedicated Webhook secret was not supplied correctly.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookAuthenticationError" + } + } + } + }, + "403": { + "description": "The target Agent has no required external secret, or the browser Origin is not allowed.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/WebhookSecretRequired" + }, + { + "$ref": "#/components/schemas/OriginRejection" + } + ] + } + } + } + }, + "404": { + "description": "The Agent does not exist or its Webhook is disabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookNotFound" + } + } + } + }, + "413": { + "description": "The JSON request body exceeds 1 MiB.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FrameworkError" + } + } + } + }, + "415": { + "description": "The request Content-Type is unsupported.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FrameworkError" + } + } + } + }, + "500": { + "description": "The Agent configuration cannot be parsed or another server failure occurred.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/WebhookConfigurationError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/agents": { + "get": { + "operationId": "listAgents", + "tags": [ + "Read-only" + ], + "summary": "List Agent summaries", + "description": "Read-only instance-owner access. Returns summaries visible to the current trusted user.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/conversations-and-runs#list-agents" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Agent summary list.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AgentList" + }, + "example": { + "agents": [ + { + "id": "your-agent-id", + "name": "Example Agent", + "skillsCount": 0, + "toolsCount": 0, + "heartbeatEnabled": false, + "hasGit": false, + "isCoreAgent": false, + "status": "idle" + } + ] + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "A browser Origin is not allowed by this deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OriginRejection" + } + } + } + }, + "500": { + "description": "An unexpected server failure occurred.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/runs": { + "get": { + "operationId": "listRuns", + "tags": [ + "Read-only" + ], + "summary": "List Run metadata", + "description": "Owner access to instance-level Run metadata. Filters combine with AND before limit is applied. This endpoint has no offset or total field and does not promise per-user isolation.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/conversations-and-runs#list-runs" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "default": 50 + }, + "description": "Single positive decimal safe integer. Repeated, empty, fractional and mixed values are rejected." + }, + { + "name": "agent_id", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Optional exact filter; combined with the other supplied filters using AND." + }, + { + "name": "delegated_by", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Optional exact filter; combined with the other supplied filters using AND." + }, + { + "name": "parent_run_id", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Optional exact filter; combined with the other supplied filters using AND." + } + ], + "responses": { + "200": { + "description": "Run metadata list; no total or offset wrapper.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunList" + }, + "example": { + "runs": [ + { + "run_id": "00000000-0000-4000-8000-000000000001", + "user_id": "example-owner", + "agent_id": "your-agent-id", + "started_at": "2026-10-14T01:00:00.000Z", + "status": "idle" + } + ] + } + } + } + }, + "400": { + "description": "The limit query value is invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsError" + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "A browser Origin is not allowed by this deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OriginRejection" + } + } + } + }, + "500": { + "description": "An unexpected server failure occurred.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/runs/{id}": { + "get": { + "operationId": "getRun", + "tags": [ + "Read-only" + ], + "summary": "Read Run metadata", + "description": "Returns the RunMeta object directly. This owner endpoint reads instance-level metadata and does not promise per-user isolation.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/conversations-and-runs#get-run" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Run ID." + } + ], + "responses": { + "200": { + "description": "Run metadata without an enclosing run field.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunMeta" + }, + "example": { + "run_id": "00000000-0000-4000-8000-000000000001", + "user_id": "example-owner", + "agent_id": "your-agent-id", + "started_at": "2026-10-14T01:00:00.000Z", + "status": "idle" + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "A browser Origin is not allowed by this deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OriginRejection" + } + } + } + }, + "404": { + "description": "The Run was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsError" + } + } + } + }, + "500": { + "description": "An unexpected server failure occurred.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/runs/{id}/runtime-state": { + "get": { + "operationId": "getRunRuntimeState", + "tags": [ + "Read-only" + ], + "summary": "Read the current raw Run state", + "description": "Preserves running, idle, completed and failed states. idle does not prove success of a particular submitted turn. A snapshot cannot replace authoritative checks performed by a mutating operation.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/conversations-and-runs#get-run-runtime-state" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]+$" + }, + "description": "Run ID." + } + ], + "responses": { + "200": { + "description": "Raw Run identity and status.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunRuntimeState" + }, + "example": { + "run_id": "00000000-0000-4000-8000-000000000001", + "agent_id": "your-agent-id", + "status": "idle" + } + } + } + }, + "400": { + "description": "The Run ID is invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsError" + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "A browser Origin is not allowed by this deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OriginRejection" + } + } + } + }, + "404": { + "description": "The Run runtime state is unavailable.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsError" + } + } + } + }, + "500": { + "description": "An unexpected server failure occurred.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/runs/{id}/receipt": { + "get": { + "operationId": "listRunReceipts", + "tags": [ + "Read-only" + ], + "summary": "Read receipts belonging to the trusted user", + "description": "The service checks trusted user ownership; a missing or foreign Run returns 404. Without offset and limit, returns all valid receipts in stored order. Supplying either parameter enables pagination from the newest receipts; effective limit is capped at 100 and offset at 10000.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/conversations-and-runs#list-run-receipts" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]+$" + }, + "description": "Run ID." + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "default": 20 + }, + "description": "Single positive safe integer. In paginated mode the effective value is capped at 100; omission defaults to 20 only when offset or limit enables pagination." + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "default": 0 + }, + "description": "Single non-negative safe integer; enables pagination and skips receipts backward from the newest. Effective value is capped at 10000." + } + ], + "responses": { + "200": { + "description": "Run receipts; pagination fields appear when pagination is requested.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RunReceipts" + }, + "example": { + "run_id": "00000000-0000-4000-8000-000000000001", + "receipts": [ + { + "receipt_id": "example-receipt-id", + "run_id": "00000000-0000-4000-8000-000000000001", + "goal": "Reply to the test message", + "input": "Please say hello.", + "output": "Hello.", + "tool_calls": [], + "outcome": { + "status": "success", + "auto_completed": true + }, + "started_at": "2026-10-14T01:00:00.000Z", + "completed_at": "2026-10-14T01:00:01.000Z" + } + ], + "total": 1, + "offset": 0, + "limit": 20, + "has_more": false, + "skipped": 0 + } + } + } + }, + "400": { + "description": "The Run ID or pagination parameter is invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsError" + } + } + } + }, + "401": { + "description": "Authentication failed or no trusted user identity could be resolved.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + ] + } + } + } + }, + "403": { + "description": "A browser Origin is not allowed by this deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OriginRejection" + } + } + } + }, + "404": { + "description": "The Run is missing or belongs to another trusted user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsError" + } + } + } + }, + "500": { + "description": "An unexpected server failure occurred.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/OperationsError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/query/start": { + "post": { + "operationId": "startAgentQuery", + "tags": [ + "Conversations" + ], + "summary": "Submit a new query", + "description": "Connect Socket.IO and subscribe to events first. Use its current socket.id as originSocketId; stable clientInstanceId belongs to Socket auth. HTTP 202 may precede durable commit and query:started.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/conversations-and-runs" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryStartRequest" + }, + "example": { + "prompt": "Please reply with a short greeting.", + "options": { + "agentId": "your-agent-id", + "conversationId": "integration-demo" + }, + "clientRequestId": "demo-start-1", + "userMessageId": "demo-message-1", + "originSocketId": "replace-with-current-socket-id" + } + } + } + }, + "responses": { + "202": { + "description": "Transport/admission acknowledgement, not proof of durable commit, execution start or completion.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryAdmission" + }, + "example": { + "ok": true, + "clientRequestId": "demo-start-1" + } + } + } + }, + "400": { + "description": "Body validation, missing current origin socket, or query admission failed.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QueryAdmission" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "HTTP/socket principal mismatch, UI-context rejection, or an unapproved Origin.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QueryAdmission" + }, + { + "$ref": "#/components/schemas/OriginRejection" + } + ] + } + } + } + }, + "409": { + "description": "Conversation clear is in progress, or the request ID was reused with a different request digest. Follow retryable/busy/code rather than retrying all conflicts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryAdmission" + } + } + } + }, + "503": { + "description": "The socket or handler is unavailable, request admission is still reserved/unknown, or the service is starting.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QueryAdmission" + }, + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/query/follow-up": { + "post": { + "operationId": "followUpAgentQuery", + "tags": [ + "Conversations" + ], + "summary": "Submit a follow-up turn", + "description": "Use the live sessionId from query:started and the current connected socket.id. Listen for query:followUpAccepted, query:message and the terminal event. Keep the same clientRequestId and frozen business body for eligible transport retries.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/conversations-and-runs" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryFollowUpRequest" + }, + "example": { + "sessionId": "replace-with-query-started-session-id", + "originSocketId": "replace-with-current-socket-id", + "clientRequestId": "demo-follow-up-1", + "message": { + "id": "demo-message-2", + "role": "user", + "content": "Please continue briefly." + } + } + } + } + }, + "responses": { + "202": { + "description": "Transport/admission acknowledgement, not proof of durable commit, execution start or completion.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryAdmission" + }, + "example": { + "ok": true, + "sessionId": "replace-with-query-started-session-id", + "clientRequestId": "demo-follow-up-1" + } + } + } + }, + "400": { + "description": "Body validation, missing current origin socket, or query admission failed.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QueryAdmission" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "HTTP/socket principal mismatch, UI-context rejection, or an unapproved Origin.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QueryAdmission" + }, + { + "$ref": "#/components/schemas/OriginRejection" + } + ] + } + } + } + }, + "409": { + "description": "Conversation clear is in progress, or the request ID was reused with a different request digest. Follow retryable/busy/code rather than retrying all conflicts.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueryAdmission" + } + } + } + }, + "503": { + "description": "The socket or handler is unavailable, request admission is still reserved/unknown, or the service is starting.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/QueryAdmission" + }, + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/query/client-requests/lookup": { + "post": { + "operationId": "lookupQueryAdmissions", + "tags": [ + "Conversations" + ], + "summary": "Reconcile query admission by stable request IDs", + "description": "Read-only admission lookup scoped to the trusted user and Agent. committed is a durable submission, not completion; reserved is an in-process reservation; absent is an observation and does not justify resending with a new ID; unknown requires reconciliation. Retention windows are returned by the service.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/conversations-and-runs" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientRequestLookupRequest" + }, + "example": { + "agentId": "your-agent-id", + "conversationId": "integration-demo", + "requests": [ + { + "clientRequestId": "demo-start-1", + "userMessageId": "demo-message-1" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Read-only admission and execution-witness observations.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClientRequestLookupResponse" + }, + "example": { + "protocolVersion": 1, + "serverInstanceId": "example-process-generation", + "observedAt": "2026-10-14T08:00:00.000Z", + "autoReplayRetentionMs": 86400000, + "tombstoneRetentionMs": 604800000, + "results": [ + { + "clientRequestId": "demo-start-1", + "status": "committed", + "committedAt": "2026-10-14T07:59:59.000Z", + "sessionId": "example-session-id", + "committedServerInstance": "current", + "executionWitness": "user-message" + } + ] + } + } + } + }, + "400": { + "description": "The lookup request does not match the Schema.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsError" + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "A browser Origin is not allowed by this deployment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OriginRejection" + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/agents/{id}/webhooks": { + "get": { + "operationId": "getAgentWebhookConfiguration", + "tags": [ + "Webhook management" + ], + "summary": "Read Webhook configuration and available Tools", + "description": "Requires capabilities.webhookManagement === true from the authenticated handshake and the existing instance-owner credential. Lists event settings and Tool IDs the owner can select. Does not disclose the current secret value.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/agent-webhook" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Agent ID from the instance, not its display name." + } + ], + "responses": { + "200": { + "description": "Read Webhook configuration and available Tools", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookManagement" + }, + "example": { + "enabled": false, + "hasSecret": false, + "events": [], + "availableTools": [] + } + } + }, + "headers": { + "Cache-Control": { + "schema": { + "type": "string", + "const": "no-store, private" + } + } + } + }, + "400": { + "description": "Invalid Agent/event ID, malformed management body, unavailable Tool ID, or no configured event/secret when enabling. Stable reason appears in error; no separate code field is required.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "An instance-owner management credential is required, or the Origin is not allowed. A trigger secret cannot manage this configuration.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/OriginRejection" + } + ] + } + } + } + }, + "404": { + "description": "The Agent or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementError" + } + } + } + }, + "409": { + "description": "Existing Webhook configuration is invalid; repair it before modifying events.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementError" + } + } + } + }, + "500": { + "description": "The configuration could not be read or saved.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "patch": { + "operationId": "setAgentWebhookEnabled", + "tags": [ + "Webhook management" + ], + "summary": "Enable or pause existing Webhook events", + "description": "Requires capabilities.webhookManagement === true from the authenticated handshake and the existing instance-owner credential. Enabling requires configured events and a dedicated secret. Pausing stops new trigger admission; it does not cancel work already accepted.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/agent-webhook" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Agent ID from the instance, not its display name." + } + ], + "responses": { + "200": { + "description": "Enable or pause existing Webhook events", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEnabledResponse" + }, + "example": { + "success": true, + "enabled": false + } + } + }, + "headers": { + "Cache-Control": { + "schema": { + "type": "string", + "const": "no-store, private" + } + } + } + }, + "400": { + "description": "Invalid Agent/event ID, malformed management body, unavailable Tool ID, or no configured event/secret when enabling. Stable reason appears in error; no separate code field is required.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "An instance-owner management credential is required, or the Origin is not allowed. A trigger secret cannot manage this configuration.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/OriginRejection" + } + ] + } + } + } + }, + "404": { + "description": "The Agent or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementError" + } + } + } + }, + "409": { + "description": "Existing Webhook configuration is invalid; repair it before modifying events.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementError" + } + } + } + }, + "500": { + "description": "The configuration could not be read or saved.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEnabledRequest" + }, + "example": { + "enabled": false + } + } + } + } + } + }, + "/api/agents/{id}/webhooks/events/{event}": { + "put": { + "operationId": "upsertAgentWebhookEvent", + "tags": [ + "Webhook management" + ], + "summary": "Create or replace an event using selected available Tool IDs", + "description": "Requires capabilities.webhookManagement === true from the authenticated handshake and the existing instance-owner credential. The service captures current Tool and file access limits. The caller cannot submit a permission snapshot. Saving enables Webhook and returns secret only when it was first generated; preserve it securely. An existing event is replaced.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/agent-webhook" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Agent ID from the instance, not its display name." + }, + { + "name": "event", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$", + "not": { + "enum": [ + "constructor", + "prototype", + "__proto__" + ] + } + }, + "description": "Case-sensitive event name; reserved prototype names are rejected." + } + ], + "responses": { + "200": { + "description": "Create or replace an event using selected available Tool IDs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEventWritten" + }, + "example": { + "success": true, + "enabled": true, + "event": "demo.ping", + "secret": "example-secret-placeholder-do-not-use-12345" + } + } + }, + "headers": { + "Cache-Control": { + "schema": { + "type": "string", + "const": "no-store, private" + } + } + } + }, + "400": { + "description": "Invalid Agent/event ID, malformed management body, unavailable Tool ID, or no configured event/secret when enabling. Stable reason appears in error; no separate code field is required.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "An instance-owner management credential is required, or the Origin is not allowed. A trigger secret cannot manage this configuration.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/OriginRejection" + } + ] + } + } + } + }, + "404": { + "description": "The Agent or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementError" + } + } + } + }, + "409": { + "description": "Agent configuration changed while preparing the permission snapshot (webhook_configuration_changed), or existing configuration is malformed (webhook_configuration_invalid). Reload configuration and available tools, review permissions, then retry; repair malformed configuration first.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementError" + } + } + } + }, + "413": { + "description": "The management body exceeds 128 KiB.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FrameworkError" + } + } + } + }, + "415": { + "description": "Use Content-Type: application/json.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FrameworkError" + } + } + } + }, + "500": { + "description": "The configuration could not be read or saved.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + }, + "x-max-body-bytes": 131072, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEventWrite" + }, + "example": { + "promptTemplate": "Confirm receipt of {{message}} without using any tools.", + "toolIds": [], + "maxTurns": 5 + } + } + } + } + }, + "delete": { + "operationId": "deleteAgentWebhookEvent", + "tags": [ + "Webhook management" + ], + "summary": "Delete one configured event", + "description": "Requires capabilities.webhookManagement === true from the authenticated handshake and the existing instance-owner credential. Removes an existing event key, including historical keys outside the new-event naming rule. URL-encode the key. Removing the final event also disables Webhook. The response reports whether Webhook remains enabled.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/agent-webhook" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Agent ID from the instance, not its display name." + }, + { + "name": "event", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Existing event key, URL-encoded. Historical keys can be deleted even if they do not satisfy the new-event naming rule." + } + ], + "responses": { + "200": { + "description": "Delete one configured event", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookEnabledResponse" + }, + "example": { + "success": true, + "enabled": false + } + } + }, + "headers": { + "Cache-Control": { + "schema": { + "type": "string", + "const": "no-store, private" + } + } + } + }, + "400": { + "description": "Invalid Agent/event ID, malformed management body, unavailable Tool ID, or no configured event/secret when enabling. Stable reason appears in error; no separate code field is required.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "An instance-owner management credential is required, or the Origin is not allowed. A trigger secret cannot manage this configuration.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/OriginRejection" + } + ] + } + } + } + }, + "404": { + "description": "The Agent or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementError" + } + } + } + }, + "409": { + "description": "Existing Webhook configuration is invalid; repair it before modifying events.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementError" + } + } + } + }, + "500": { + "description": "The configuration could not be read or saved.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/agents/{id}/webhooks/secret/rotate": { + "post": { + "operationId": "rotateAgentWebhookSecret", + "tags": [ + "Webhook management" + ], + "summary": "Rotate the Agent dedicated Webhook secret", + "description": "Requires capabilities.webhookManagement === true from the authenticated handshake and the existing instance-owner credential. Send no request body. The new secret applies to all events for this Agent. Save the returned value and update external senders; ordinary configuration reads cannot recover it.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/agent-webhook" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Agent ID from the instance, not its display name." + } + ], + "responses": { + "200": { + "description": "Rotate the Agent dedicated Webhook secret", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WebhookSecretRotation" + }, + "example": { + "secret": "example-secret-placeholder-do-not-use-12345" + } + } + }, + "headers": { + "Cache-Control": { + "schema": { + "type": "string", + "const": "no-store, private" + } + } + } + }, + "400": { + "description": "Invalid Agent/event ID, malformed management body, unavailable Tool ID, or no configured event/secret when enabling. Stable reason appears in error; no separate code field is required.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "An instance-owner management credential is required, or the Origin is not allowed. A trigger secret cannot manage this configuration.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/OriginRejection" + } + ] + } + } + } + }, + "404": { + "description": "The Agent or event was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementError" + } + } + } + }, + "409": { + "description": "Existing Webhook configuration is invalid; repair it before modifying events.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ManagementError" + } + } + } + }, + "500": { + "description": "The configuration could not be read or saved.", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ManagementError" + }, + { + "$ref": "#/components/schemas/FrameworkError" + } + ] + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/agents/{agentId}/schedule-messages": { + "get": { + "operationId": "listAgentScheduleMessages", + "tags": [ + "Read-only" + ], + "summary": "Read visible persisted background messages", + "description": "Returns only messages visible to the trusted user, filtering recipient and source ACL before pagination. Webhook delivery rows contain explicit SendUserMessage output, not every final model response or a per-request terminal result. Items are sorted newest first; limit defaults to 50 and is capped at 200.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/agent-webhook" + }, + "security": [ + { + "InstanceToken": [] + } + ], + "parameters": [ + { + "name": "agentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]+$" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 9007199254740991, + "default": 50 + }, + "description": "Positive safe integer; effective page size is capped at 200." + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991, + "default": 0 + } + } + ], + "responses": { + "200": { + "description": "Visible background messages and effective pagination.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScheduleMessagePage" + }, + "example": { + "items": [], + "total": 0, + "limit": 50, + "offset": 0 + } + } + } + }, + "400": { + "description": "Pagination or source authorization failed; use the returned error message.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsError" + } + } + } + }, + "401": { + "description": "The instance credential is missing or invalid.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InstanceAuthenticationError" + } + } + } + }, + "403": { + "description": "Pagination or source authorization failed; use the returned error message.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsError" + } + } + } + }, + "500": { + "description": "Pagination or source authorization failed; use the returned error message.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OperationsError" + } + } + } + }, + "503": { + "description": "Startup is incomplete or instance authentication is not initialized.", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ServiceStarting" + }, + { + "$ref": "#/components/schemas/InstanceTokenUninitialized" + } + ] + } + } + }, + "headers": { + "Retry-After": { + "schema": { + "type": "string" + }, + "description": "Present for service_not_ready; retry the read-only request after this many seconds." + } + } + }, + "default": { + "description": "Other service or reverse-proxy failures. Preserve the HTTP status, Content-Type and response body; formats are not uniform.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "InstanceToken": { + "type": "apiKey", + "in": "header", + "name": "X-Agent-Service-Token", + "description": "Existing owner credential supplied by the instance administrator." + }, + "WebhookSecret": { + "type": "http", + "scheme": "bearer", + "description": "Dedicated secret for the target Agent Webhook; not a provider API key or JWT." + } + }, + "schemas": { + "ClientHandshake": { + "type": "object", + "required": [ + "service", + "apiProtocolVersion", + "serverVersion", + "instanceId", + "deploymentMode", + "clientCompatibility", + "capabilities", + "authentication", + "serverTime" + ], + "properties": { + "service": { + "type": "string", + "const": "desirecore-agent-service", + "description": "固定服务标记,用于拒绝非 DesireCore Agent Service 目标。" + }, + "apiProtocolVersion": { + "type": "string", + "const": "1", + "description": "客户端握手和实例连接协议版本;不等同于应用版本。" + }, + "serverVersion": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", + "description": "当前 Agent Service 随客户端发布的语义版本号。" + }, + "instanceId": { + "type": "string", + "pattern": "^inst_[A-Za-z0-9_-]{32}$", + "description": "Agent Service 数据空间的稳定不透明身份。由服务端随机生成并持久化,不得由路径、用户、设备或 token 推导。" + }, + "deploymentMode": { + "type": "string", + "enum": [ + "embedded", + "owner", + "external" + ], + "description": "Agent Service 运行形态:embedded=Electron 内嵌;owner=standalone owner 模式;external=standalone external 模式。" + }, + "clientCompatibility": { + "type": "object", + "required": [ + "minimumVersion", + "maximumVersion" + ], + "properties": { + "minimumVersion": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", + "description": "允许连接本服务的最低 DesireCore 客户端版本(含)。" + }, + "maximumVersion": { + "type": [ + "string", + "null" + ], + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", + "description": "允许连接本服务的最高 DesireCore 客户端版本(含);null 表示不设上限。" + } + }, + "additionalProperties": false + }, + "capabilities": { + "type": "object", + "required": [ + "workflow", + "mediaSignedUrl" + ], + "properties": { + "workflow": { + "type": "boolean", + "description": "当前部署是否具备可用且完整配置的 Workflow Service 能力。" + }, + "pdfRendering": { + "type": "boolean", + "description": "服务端宿主是否提供 HTML 转 PDF 引擎;缺席表示未知,不代表客户端具有打印传输。" + }, + "mediaSignedUrl": { + "type": "boolean", + "description": "当前实例是否可以签发供原生媒体标签使用的短期 HTTPS URL。" + }, + "queryRequestIdEcho": { + "type": "boolean", + "description": "服务端是否回显客户端 query 的 clientRequestId;缺席表示未知,客户端只能按实际事件观察,禁止按版本推断。" + }, + "webhookManagement": { + "type": "boolean", + "description": "是否支持所有者通过 Webhook 管理 API 配置事件、选择现有工具和轮换 secret;缺席或 false 表示该配置入口不可用。" + }, + "localPairing": { + "type": "boolean" + }, + "localAttachSecret": { + "type": "boolean" + }, + "objectStorageCapability": { + "type": "boolean" + }, + "mailCapability": { + "type": "boolean" + } + }, + "additionalProperties": { + "type": "boolean", + "description": "未来新增的布尔能力位;旧客户端应忽略不认识的能力名。" + } + }, + "authentication": { + "type": "object", + "required": [ + "kind", + "scopes" + ], + "properties": { + "kind": { + "type": "string", + "enum": [ + "embedded-capability", + "instance-token" + ], + "description": "本次服务形态使用的客户端鉴权类型:embedded-capability 或 standalone instance-token。" + }, + "scopes": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "owner" + ] + }, + "description": "认证凭据授予的实例作用域;第一版固定为 owner。" + } + }, + "additionalProperties": false + }, + "serverTime": { + "type": "string", + "format": "date-time", + "description": "服务端生成响应时的 ISO 8601 时间,用于诊断时钟偏差,不参与身份判断。" + }, + "product": { + "type": "object", + "required": [ + "editionId", + "appId", + "productName", + "buildKind", + "appVersion" + ], + "properties": { + "editionId": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*$", + "maxLength": 128, + "description": "服务所在应用的发行版 ID;同机接入发现据此标注候选实例属于哪个发行版。" + }, + "appId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "服务所在应用的 appId(macOS bundle id、Windows AUMID 前缀)。" + }, + "productName": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "服务所在应用的显示名。" + }, + "buildKind": { + "type": "string", + "enum": [ + "dev", + "prod" + ], + "description": "服务所在应用的构建形态:Electron 打包版为 prod,开发版为 dev;standalone 按运行环境判断。" + }, + "appVersion": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", + "description": "服务所在应用的版本号,与 serverVersion 相同。" + } + }, + "additionalProperties": false + }, + "capabilityDiagnostics": { + "type": "object", + "required": [ + "pdfRendering" + ], + "properties": { + "pdfRendering": { + "type": [ + "string", + "null" + ], + "enum": [ + "pdf_renderer_unavailable", + null + ] + } + }, + "additionalProperties": false, + "description": "仅协商 deployment-capabilities 时返回;原因是部署能力事实,不含配置、路径或凭据。" + }, + "queryDispatch": { + "type": "object", + "required": [ + "serverInstanceId" + ], + "properties": { + "serverInstanceId": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "当前服务进程启动代际,与 recovery.serverInstanceId 同源;重启即变化,不能当持久 instanceId 或认证凭证。" + } + }, + "additionalProperties": false, + "description": "仅协商 query-dispatch 扩展时返回;旧服务缺席本字段时不得伪造进程身份。" + } + }, + "additionalProperties": false + }, + "WebhookRequest": { + "type": "object", + "required": [ + "event" + ], + "properties": { + "event": { + "type": "string", + "description": "事件标识,如 \"github.push\"" + }, + "payload": { + "type": "object", + "additionalProperties": {}, + "description": "事件负载,prompt_template 中的 {{key}} 从此处提取" + } + }, + "additionalProperties": false + }, + "WebhookAccepted": { + "type": "object", + "properties": { + "accepted": { + "type": "boolean", + "const": true + }, + "agentId": { + "type": "string" + }, + "event": { + "type": "string" + } + }, + "required": [ + "accepted", + "agentId", + "event" + ], + "additionalProperties": false + }, + "InstanceAuthenticationError": { + "type": "object", + "properties": { + "error": { + "type": "string", + "const": "unauthorized" + }, + "message": { + "type": "string" + } + }, + "required": [ + "error", + "message" + ], + "additionalProperties": false + }, + "OriginRejection": { + "type": "object", + "properties": { + "error": { + "type": "string", + "const": "origin_not_allowed" + }, + "message": { + "type": "string" + } + }, + "required": [ + "error" + ], + "additionalProperties": true + }, + "ServiceStarting": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "starting" + }, + "service": { + "type": "string", + "const": "agent-service" + }, + "code": { + "type": "string", + "const": "service_not_ready" + } + }, + "required": [ + "status", + "service", + "code" + ], + "additionalProperties": false + }, + "InstanceTokenUninitialized": { + "type": "object", + "properties": { + "error": { + "type": "string", + "const": "instance_token_uninitialized" + }, + "message": { + "type": "string" + } + }, + "required": [ + "error", + "message" + ], + "additionalProperties": false + }, + "WebhookAuthenticationError": { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": [ + "Invalid webhook secret", + "Invalid embedded instance token" + ] + } + }, + "required": [ + "error" + ], + "additionalProperties": false + }, + "WebhookSecretRequired": { + "type": "object", + "properties": { + "error": { + "type": "string", + "const": "webhook_secret_required" + }, + "message": { + "type": "string" + } + }, + "required": [ + "error", + "message" + ], + "additionalProperties": false + }, + "WebhookNotFound": { + "type": "object", + "properties": { + "error": { + "type": "string", + "enum": [ + "Agent not found", + "Webhook not enabled for this agent" + ] + } + }, + "required": [ + "error" + ], + "additionalProperties": false + }, + "WebhookInvalidBody": { + "type": "object", + "properties": { + "error": { + "type": "string", + "const": "Invalid request body" + }, + "details": { + "type": "array", + "items": {} + } + }, + "required": [ + "error", + "details" + ], + "additionalProperties": false + }, + "WebhookUnknownEvent": { + "type": "object", + "properties": { + "error": { + "type": "string", + "pattern": "^Unknown event type: " + } + }, + "required": [ + "error" + ], + "additionalProperties": false + }, + "WebhookConfigurationError": { + "type": "object", + "properties": { + "error": { + "type": "string", + "const": "Failed to parse agent.json" + } + }, + "required": [ + "error" + ], + "additionalProperties": false + }, + "FrameworkError": { + "type": "object", + "properties": { + "statusCode": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "statusCode", + "error", + "message" + ], + "additionalProperties": true + }, + "AgentSummary": { + "type": "object", + "required": [ + "id", + "name", + "skillsCount", + "toolsCount", + "heartbeatEnabled", + "hasGit", + "isCoreAgent" + ], + "properties": { + "id": { + "type": "string", + "description": "智能体 ID(目录名)" + }, + "name": { + "type": "string", + "description": "智能体显示名称" + }, + "description": { + "type": "string", + "description": "智能体描述" + }, + "ui": { + "type": "object", + "description": "该智能体的界面呈现配置,仅作用于其会话,不改变平台设置", + "properties": { + "chat_bubble_mode": { + "type": "string", + "enum": [ + "flat", + "classic" + ], + "description": "消息样式:flat 为通栏,classic 为气泡;省略时跟随平台消息样式" + } + }, + "additionalProperties": false + }, + "isCoreAgent": { + "type": "boolean" + }, + "skillsCount": { + "type": "number", + "description": "技能数量", + "minimum": 0 + }, + "toolsCount": { + "type": "number", + "description": "工具数量", + "minimum": 0 + }, + "heartbeatEnabled": { + "type": "boolean", + "description": "心跳是否启用" + }, + "engine": { + "type": "string", + "description": "运行时引擎" + }, + "hasGit": { + "type": "boolean", + "description": "是否有 git 仓库" + }, + "hasRemote": { + "type": "boolean" + }, + "status": { + "type": "string", + "description": "Agent 运行时状态(从 AgentRegistry 获取)", + "enum": [ + "online", + "busy", + "idle", + "offline", + "error", + "recovery", + "shutdown" + ] + }, + "metadataError": { + "type": "string", + "enum": [ + "missing", + "unreadable", + "invalid_json", + "invalid_shape" + ] + }, + "syncStatus": { + "type": "string", + "description": "Git 同步状态:synced(已同步)、ahead(本地有未推送提交)、behind(远程有更新)、conflict(存在冲突)、local_only(纯本地,无远程仓库)、uninitialized(未初始化 Git)", + "enum": [ + "synced", + "ahead", + "behind", + "conflict", + "local_only", + "uninitialized" + ] + }, + "avatar": { + "type": "object", + "description": "头像配置(来自 agent.json)", + "properties": { + "char": { + "type": "string", + "description": "显示字符" + }, + "color": { + "type": "string", + "description": "色系键名", + "enum": [ + "green", + "blue", + "purple", + "orange", + "red" + ] + }, + "image": { + "type": "object", + "description": "图片头像配置(agent.json 原样透传)。**这里只说明配置存在,不代表文件可用**——判断能否渲染图片请看顶层 avatarImage", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string", + "description": "相对于 Agent 根目录的图片路径" + } + }, + "additionalProperties": false + }, + "model": { + "type": "object", + "description": "沉浸式交互使用的 Agent 自定义 3D 模型配置", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string", + "description": "相对于 Agent 根目录的 GLB 文件路径" + }, + "body": { + "type": "string", + "description": "TalkingHead 模型体型提示", + "enum": [ + "F", + "M" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "avatarImage": { + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string", + "description": "缓存版本戳(`${size}-${mtimeMs}`),与该端点的 ETag 同源。值变化即表示图片已更换,客户端应丢弃旧缓存重新取图。不写进 agent.json:那会产生 git diff 噪声,且 Agent 直接覆盖图片文件时字段不会变,必然漂移。" + }, + "mimeType": { + "type": "string", + "description": "经 magic bytes 嗅探确认的真实 MIME 类型(不信任扩展名)", + "enum": [ + "image/png", + "image/jpeg", + "image/webp" + ] + } + }, + "additionalProperties": false + }, + "unread": { + "type": "number", + "description": "该 Agent 当前未读消息计数(手动多会话模式下为各话题未读之和,非手动模式为单会话计数);无未读时不返回此字段" + }, + "pinned": { + "type": "boolean", + "description": "该 Agent 是否被用户置顶在侧栏最上方(来自用户级 preferences/agent-list.json);未置顶时不返回此字段" + }, + "pinnedAt": { + "type": "string", + "description": "置顶时间 ISO 8601,多个置顶 Agent 之间的排序依据;未置顶时不返回此字段" + }, + "folded": { + "type": "boolean", + "description": "该 Agent 是否被收进侧栏的「折叠的聊天」分组(来自用户级 preferences/agent-list.json);未折叠时不返回此字段" + }, + "groupOverride": { + "type": "string", + "enum": [ + "general", + "specialist" + ], + "description": "用户手动指定的侧栏分组,覆盖默认派生(默认派生 = 团队组长自动提升为通用,其余按 Agent 自身性质);未手动指定时不返回此字段" + }, + "nestMembers": { + "type": "boolean", + "description": "该 Agent 作为团队组长时,其成员是否收纳到组长名下;用户级 preferences/agent-list.json 的显式选择优先,否则遵循团队 memberDisplay 默认值。未开启时不返回此字段" + } + }, + "additionalProperties": false + }, + "RunMeta": { + "type": "object", + "required": [ + "run_id", + "user_id", + "agent_id", + "started_at", + "status" + ], + "properties": { + "run_id": { + "type": "string", + "description": "Run UUID,全局唯一标识符,用于查询和关联 Receipt" + }, + "user_id": { + "type": "string", + "description": "发起用户 ID" + }, + "agent_id": { + "type": "string", + "description": "Agent ID(对应 AgentFS 中的目录名)" + }, + "conversation_id": { + "type": "string", + "description": "对话 ID,用于关联同一对话的多次 Run(支持多轮对话场景)" + }, + "title": { + "type": "string", + "description": "会话标题(手动多会话模式下使用):用户重命名后的自定义标题,或系统据首条消息生成的摘要标题" + }, + "title_source": { + "type": "string", + "enum": [ + "custom", + "auto" + ], + "description": "title 的来源:custom=用户手动重命名,auto=系统据首条用户消息自动生成(截断或 LLM 摘要)" + }, + "origin": { + "type": "string", + "enum": [ + "user", + "delegation" + ], + "description": "会话创建来源(手动多会话模式下使用):user=用户手动新建,delegation=被其他 Agent 委派时自动新建" + }, + "background": { + "type": "boolean" + }, + "inbound": { + "type": "boolean", + "description": "这一轮是不是「另一个 Agent 发来的消息」(SendMessage 来信)。**与 background 正交**:background 回答「该不该抢会话的前台控制槽」,本字段回答「这一轮在做什么」。来信已回归前台语义(与用户消息同路径),因此不再带 background;但「对方正在处理别人的来信」与「对方正在与用户对话」对**是否该打断**的判断截然不同,需要一个独立标记(见 agent/agent-activity.ts 的 kind 派生)。同 background,它描述的是**当前这一轮**,同一条 run 被复用时按当轮改写。" + }, + "started_at": { + "type": "string", + "description": "开始时间(ISO 8601 格式,如 2024-01-15T10:30:00Z)", + "format": "date-time" + }, + "turn_started_at": { + "type": "string", + "description": "**当前这一轮**开始执行的时间(ISO 8601)。同一条 run 会跨轮复用,started_at 停在第一次创建的时刻——一条聊了三天的会话,它的 started_at 就是三天前,拿来算「已运行多久」会报出「已运行 3 天」这种荒谬值,并把忙碌载荷的等待/打断建议带偏。每次把 run 置为 running 的路径(创建、复用、activateRun、activateSessionMetaRun)都要刷新它。updatedAt 不能替代:它会被每次 appendMessage 推后,代表的是「最后一次活动」而非「本轮起点」。", + "format": "date-time" + }, + "completed_at": { + "type": "string", + "description": "完成时间(ISO 8601 格式),仅 status 为 completed 或 failed 时有值", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "运行状态:running(执行中)、idle(空闲,等待下一轮对话)、completed(成功完成)、failed(执行失败,查看 failure_reason 字段了解原因)", + "enum": [ + "running", + "idle", + "completed", + "failed" + ] + }, + "failure_reason": { + "type": "string", + "description": "失败原因(仅 status=failed 时有值):process_restart=进程重启时被回收的孤儿 run;timeout=执行超时被强制终结;stale=历史兼容值(旧版本曾把超过复用窗口的 idle run 错标为失败);error=查询过程抛出错误", + "enum": [ + "process_restart", + "timeout", + "stale", + "error" + ] + }, + "interruption_reason": { + "type": "string", + "description": "最近一次未完成轮次的主动中止原因。Run 会回到 idle 以保留多轮会话复用能力,完整审计信息同时写入 Receipt", + "enum": [ + "user_abort", + "agent_interrupt", + "reset", + "system_suspend", + "process_shutdown", + "process_restart", + "orphan_reclaimed", + "background_timeout" + ] + }, + "interrupted_at": { + "type": "string", + "description": "最近一次主动中止发生时间(ISO 8601)。仅 interruption_reason 存在时有值", + "format": "date-time" + }, + "pending_synthesis": { + "type": "object", + "required": [ + "status", + "interruption_reason", + "created_at", + "completed_tool_calls" + ], + "properties": { + "status": { + "type": "string", + "const": "required", + "description": "固定为 required:已完成工具事实存在,但最终业务解释尚未正常收口" + }, + "interruption_reason": { + "type": "string", + "enum": [ + "user_abort", + "agent_interrupt", + "reset", + "system_suspend", + "process_shutdown", + "process_restart", + "orphan_reclaimed", + "background_timeout" + ] + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "待综合状态创建时间(ISO 8601)" + }, + "completed_tool_calls": { + "type": "array", + "minItems": 1, + "maxItems": 128, + "items": { + "type": "object", + "required": [ + "invocation_id", + "tool_name" + ], + "properties": { + "invocation_id": { + "type": "string", + "maxLength": 256, + "description": "已经成功完成的工具调用 ID,用于恢复时识别既成事实并避免重复执行" + }, + "tool_name": { + "type": "string", + "maxLength": 256, + "description": "已经成功完成的模型可见工具名称,供审计和恢复提示使用" + } + }, + "additionalProperties": false + }, + "description": "恢复时必须复用、不得仅为重取同一结果而再次执行的成功工具调用" + } + }, + "additionalProperties": false, + "description": "系统中断发生在工具成功之后、最终解释完成之前时持久化的恢复状态;正常综合成功后清除" + }, + "interrupted_turn_facts": { + "type": "object", + "required": [ + "interruption_reason", + "recorded_at", + "deliveries" + ], + "properties": { + "interruption_reason": { + "type": "string", + "description": "被记录轮次的中止原因(与 interruption_reason 字段同一枚举)。user_abort 时消费方可下「被用户停止」断言,其余原因只能弱化归因", + "enum": [ + "user_abort", + "agent_interrupt", + "reset", + "system_suspend", + "process_shutdown", + "process_restart", + "orphan_reclaimed", + "background_timeout" + ] + }, + "recorded_at": { + "type": "string", + "format": "date-time" + }, + "deliveries": { + "type": "array", + "maxItems": 64, + "items": { + "type": "object", + "required": [ + "tool_name", + "preview", + "attachment_count", + "delivery_status" + ], + "properties": { + "tool_name": { + "type": "string", + "maxLength": 256, + "description": "交付类工具的模型可见名称(builtin-tools/tool-effects 的 effect.class===\"delivery\" 派生名单成员,含历史 alias)" + }, + "preview": { + "type": "string", + "maxLength": 200, + "description": "已投递内容的开头片段(取调用 input.message / input.content 的前 50 字符)。空串表示 input 缺失或非字符串(如 MailOperations 这类通用代理),消费方必须按「无法确认实际送达」的弱化措辞处理,不得下已送达断言" + }, + "attachment_count": { + "type": "integer", + "minimum": 0, + "description": "调用 input.attachments 数组长度;0 表示无附件" + }, + "delivery_status": { + "type": "string", + "enum": [ + "completed", + "failed", + "interrupted" + ] + } + }, + "additionalProperties": false + }, + "description": "被中断轮内交付类工具调用的终态清单(tool-effects 派生名单 + 已结算终态;同 toolCallId 去重;超上限时保留最近的条目)。空数组是权威事实「该轮没有任何交付」,消费方不得再退回尾窗启发式反推" + } + }, + "additionalProperties": false + }, + "engine": { + "type": "string", + "description": "运行时引擎" + }, + "session_id": { + "type": "string", + "description": "关联的 Socket.IO session ID,用于实时通信和中断控制" + }, + "messageCount": { + "type": "number", + "description": "消息总数(由 SessionStore 维护)" + }, + "updatedAt": { + "type": "string", + "description": "最后更新时间(由 SessionStore 维护)" + }, + "cleared": { + "type": "boolean", + "description": "Tombstone 标记:用户清除对话后置为 true。会话列表和恢复逻辑会过滤掉此类 run,但 receipts/ 子目录保留,活动记录仍能读到该 run 的真实 token 消耗凭据(审计用)" + }, + "cleared_at": { + "type": "string", + "description": "Tombstone 写入时间(ISO 8601)。检查点清理按此时间计算 deletedRunRetentionMinutes;旧数据缺失时兼容 cleared=true", + "format": "date-time" + }, + "archived_at": { + "type": "string", + "description": "会话归档时间(ISO 8601)。归档是独立于运行 status 的历史生命周期,归档 run 不再自动恢复,但消息和审计记录仍保留", + "format": "date-time" + }, + "handoffFrom": { + "type": "string", + "description": "转交来源 Run ID(C-9.3):本 run 承接自该 run 的会话转交(handoff)。回撤不跨转交边界,仅展示\"承接自 X\"链路" + }, + "handoffTo": { + "type": "string", + "description": "转交目标 Run ID(C-9.3):本 run 已将会话转交给该 run。回撤不跨转交边界,仅展示\"已转交给 X\"链路" + }, + "team_id": { + "type": "string", + "description": "本 run 执行时所处的团队作用域。委派续跑(DelegateControl action=\"resume\")必须按它重建执行环境——用调用方**当前**的 teamId 会让续跑落进另一个团队的 prompt 与共享工作目录,带着旧团队的历史在新环境里接力;缺省表示该 run 不属于任何团队。" + }, + "work_context_id": { + "type": "string", + "minLength": 1, + "description": "本 Run 所属的稳定 Work Context ID。continue/resume 必须沿用,isolated/fork 必须记录新建结果;旧 Run 可缺省并由幂等 legacy backfill 建段。" + }, + "execution_attachment": { + "type": "object", + "required": [ + "userId", + "agentId", + "runId" + ], + "properties": { + "userId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$" + }, + "agentId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "执行该 Run 的权威 Agent ID。" + }, + "runId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "承载本次执行的 DesireCore Run ID。" + }, + "conversationId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "可选的用户可见 Conversation ID;缺失表示该 Run 不属于对话时间线。" + }, + "workContextId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "可选的稳定 Work Context ID,用于跨续跑保持同一工作语境。" + }, + "workflowRunId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "可选的 Workflow Run ID;与 DesireCore Run ID 正交。" + }, + "cellId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "可选的执行 Cell ID,表示服务实例所在的逻辑隔离单元。" + }, + "serviceInstanceId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "可选的 Agent Service 实例 ID;出现时必须同时提供其所属 cellId。" + }, + "jobId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$" + }, + "attemptId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$" + }, + "capsuleId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$" + } + }, + "allOf": [ + { + "if": { + "required": [ + "serviceInstanceId" + ] + }, + "then": { + "required": [ + "cellId" + ] + } + }, + { + "if": { + "required": [ + "attemptId" + ] + }, + "then": { + "required": [ + "jobId" + ] + } + }, + { + "if": { + "required": [ + "capsuleId" + ] + }, + "then": { + "required": [ + "jobId", + "attemptId" + ] + } + } + ], + "additionalProperties": false, + "description": "本 Run 的权威执行归属;新 Run 由服务端构造,旧 Run 可缺失。其 user/agent/run/conversation/work-context 必须与外围 RunMeta 一致。" + }, + "work_context_selection": { + "type": "object", + "required": [ + "context_mode", + "origin" + ], + "properties": { + "context_mode": { + "type": "string", + "enum": [ + "continue", + "isolated", + "fork" + ], + "description": "必填且无默认值:continue=继续明确存在的同一任务上下文;isolated=不同新任务;fork=同一任务有意分叉。" + }, + "origin": { + "type": "string", + "enum": [ + "delegate", + "mention", + "task", + "workflow", + "schedule", + "webhook", + "decision", + "legacy" + ] + }, + "work_context_id": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "description": "continue 必填:要继续的稳定 Work Context ID;禁止回退到目标 Agent 最近一次会话。" + }, + "source_work_context_id": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "description": "fork 必填:有意分叉的来源 Work Context ID。" + }, + "task_ref": { + "type": "object", + "required": [ + "board_id", + "task_id" + ], + "properties": { + "board_id": { + "type": "string", + "minLength": 1 + }, + "task_id": { + "type": "string", + "minLength": 1, + "description": "TaskBoard 内稳定 Task ID。" + }, + "task_version": { + "type": "integer", + "minimum": 1, + "description": "选择时观察到的 Task 版本快照;身份查重只使用 board_id + task_id。" + } + }, + "additionalProperties": false + }, + "intent_id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "context_reason": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "description": "isolated/fork 必填:说明为何这是不同新任务或为何允许对同一 Task 有意分叉。" + } + }, + "allOf": [ + { + "if": { + "properties": { + "context_mode": { + "const": "continue" + } + }, + "required": [ + "context_mode" + ] + }, + "then": { + "required": [ + "work_context_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "source_work_context_id" + ] + }, + { + "required": [ + "intent_id" + ] + } + ] + } + } + }, + { + "if": { + "properties": { + "context_mode": { + "const": "isolated" + } + }, + "required": [ + "context_mode" + ] + }, + "then": { + "required": [ + "context_reason" + ], + "anyOf": [ + { + "required": [ + "task_ref" + ] + }, + { + "required": [ + "intent_id" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "work_context_id" + ] + }, + { + "required": [ + "source_work_context_id" + ] + } + ] + } + } + }, + { + "if": { + "properties": { + "context_mode": { + "const": "fork" + } + }, + "required": [ + "context_mode" + ] + }, + "then": { + "required": [ + "source_work_context_id", + "task_ref", + "context_reason" + ], + "not": { + "anyOf": [ + { + "required": [ + "work_context_id" + ] + }, + { + "required": [ + "intent_id" + ] + } + ] + } + } + } + ], + "additionalProperties": false, + "description": "启动此 Run 时调用方显式提交并经 preflight/commit 接受的 Work Context 选择快照。" + }, + "work_context_task_ref": { + "type": "object", + "required": [ + "board_id", + "task_id" + ], + "properties": { + "board_id": { + "type": "string", + "minLength": 1 + }, + "task_id": { + "type": "string", + "minLength": 1, + "description": "TaskBoard 内稳定 Task ID。" + }, + "task_version": { + "type": "integer", + "minimum": 1, + "description": "选择时观察到的 Task 版本快照;身份查重只使用 board_id + task_id。" + } + }, + "additionalProperties": false + }, + "work_context_intent_id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "work_context_origin": { + "type": "string", + "enum": [ + "delegate", + "mention", + "task", + "workflow", + "schedule", + "webhook", + "decision", + "legacy" + ], + "description": "此 Run 进入 Work Context 的权威入口快照。" + }, + "child_context": { + "type": "object", + "description": "原委派对子 Agent 上下文组装施加的**收紧**覆盖(Delegate 的 childContext 解析结果)。续跑必须按它重建,否则「只传 run_id 的裸续跑」会让原本被刻意收窄的上下文(memoryScope: none、关掉的能力、section 过滤)在续跑时全部放开——收紧只能保持或更严。形状与 ContextAssemblyOptions 的对应覆盖字段一致;缺省表示原委派未收紧。", + "additionalProperties": true + }, + "child_tool_ceiling": { + "type": "object", + "description": "委派执行时的工具键与文件访问范围快照。tools 仅记录执行方当时的工具作为审计证据,不是父智能体的授权上限;持久 Agent 续跑使用执行方当前工具权限,并保留防递归限制。allowedReadDirs / allowedReadPaths / allowedWriteDirs 继续与调用方当前范围取交集;缺省的文件维度表示无限制。旧快照的工具键同样不再裁剪目标工具,形状错误仍拒绝恢复。", + "additionalProperties": true + }, + "work_dir": { + "type": "string", + "description": "本 run 起跑时解析出的**基准**工作目录(团队共享目录 > 手动话题 Project > Agent/全局 effectiveCwd;不含运行时 worktree 绑定那层派生覆盖)。委派续跑据此确认「原来那次是在哪个目录里跑的」:承接来的历史里全是对旧目录中文件的读写,工作目录偏好在两次之间改过的话,续跑就会带着旧上下文在另一个项目里接力。⚠ 与 worktree_cwd 同一约定:**空字符串表示「起跑时确实没解析出工作目录」,字段缺席表示「该 run 创建于本字段引入之前」**——两者混为一谈的话,「当初没配、现在配了工作目录」会被闸门当成老数据跳过,而承接来的历史其实来自当时的进程默认目录。" + }, + "worktree_cwd": { + "type": "string", + "description": "本 run 起跑时实际附着的 Owner worktree 目录(`work_dir` 之上的派生覆盖)。与 work_dir 分开记:前者是用户偏好解析出的基准目录,这一条是运行时按 Owner 绑定的链接工作树。委派续跑两者都要一致——只比基准目录的话,源 run 当初挂在某个 worktree 上、而绑定后来被退出或改挂时闸门会放行,续跑于是回到主 checkout 或另一个工作树里接力。⚠ **空字符串表示「起跑时确实没有附着任何 worktree」,字段缺席表示「该 run 创建于本字段引入之前,无从得知」**——两者必须可区分:都写成缺席的话,「当初没挂、现在挂上了」这一格会被当成老数据跳过比较,而它恰恰是要拦的(历史来自主 checkout,续跑却跑进新工作树)。因此起跑路径**总是**写这个字段。" + }, + "resumedFrom": { + "type": "string", + "description": "同一执行连续性的来源 Run ID:由 DelegateControl(action=\"resume\") 或已接纳 Work Context continue 承接该 run。承接来的历史只作为运行时上下文喂给模型,**不落进本 run 的 messages.jsonl**,因此二次续跑必须沿这条链回溯收集,否则每续一次就丢掉更早那一段的历史与工具结果。与 handoffFrom 的区别:那是会话转交(用户换了对话对象),这是同一委派任务/Work Context 的接力。" + }, + "delegation": { + "type": "object", + "description": "委派溯源:仅由 Delegate 工具创建/复用的 run 有值,记录本 run 由哪个 Agent 的哪个父 run 委派产生。async 场景下父消息 metadata.delegate.children 在工具返回时为空,可通过此字段反查委派链(InspectRuns 工具 action=list 依赖它过滤子 run)", + "required": [ + "fromAgentId" + ], + "properties": { + "fromAgentId": { + "type": "string", + "description": "发起委派的 Agent ID(delegation.fromAgentId,用户直接发起时为 owner)" + }, + "parentRunId": { + "type": "string", + "description": "委派方发起委派时所在的父 run ID。由父 sessionId 经 getRunIdBySession 反查;反查失败时缺省" + }, + "parentClientRequestId": { + "type": "string", + "minLength": 1, + "description": "委派子 Run 创建时,由 ToolExecutor 从父 Agent **当前轮次**注入的可信 clientRequestId。它与可复用的 parentRunId 分开记录,用于证明子 Run 确实由父 Run 的这一轮产生;旧 Run 可缺省,但需要轮次级授权的新入口必须在缺失时失败关闭。" + }, + "mode": { + "type": "string", + "enum": [ + "sync", + "async", + "via-messaging", + "fan-out", + "subtask" + ], + "description": "委派模式(handoff 不写 delegation,走 handoffFrom/handoffTo 链路;worker 无独立 run)。subtask=继承发起方会话上下文的子体,历史来源另记在 subtask_inherited_from。" + } + }, + "additionalProperties": false + }, + "delivery_routing": { + "type": "object", + "required": [ + "group", + "audience" + ], + "properties": { + "group": { + "type": "string", + "enum": [ + "delegator-governed", + "user-handoff", + "run-inherited" + ], + "description": "委派交付路由分组:delegator-governed=先回委派方;user-handoff=目标会话直接服务用户;run-inherited=沿用目标 Run 已冻结受众。" + }, + "audience": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "conversationId" + ], + "properties": { + "type": { + "type": "string", + "const": "user", + "description": "交付给当前用户可见会话。" + }, + "conversationId": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "用户接收交付的稳定会话 ID。" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "type", + "agentId" + ], + "properties": { + "type": { + "type": "string", + "const": "delegator", + "description": "交付给发起委派的主 Agent。" + }, + "agentId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "委派方规范 Agent ID。" + }, + "parentRunId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "发起委派时的父 Run ID。" + }, + "conversationId": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "异步结果必须回到的父会话 ID。" + }, + "correlationId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "请求与最终交付的稳定关联 ID。" + } + }, + "additionalProperties": false + } + ], + "description": "准入时冻结的真实交付受众;只允许用户会话或委派方 Agent。" + } + }, + "additionalProperties": false, + "allOf": [ + { + "if": { + "properties": { + "group": { + "const": "delegator-governed" + } + }, + "required": [ + "group" + ] + }, + "then": { + "properties": { + "audience": { + "type": "object", + "required": [ + "type", + "agentId" + ], + "properties": { + "type": { + "type": "string", + "const": "delegator", + "description": "交付给发起委派的主 Agent。" + }, + "agentId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "委派方规范 Agent ID。" + }, + "parentRunId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "发起委派时的父 Run ID。" + }, + "conversationId": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "异步结果必须回到的父会话 ID。" + }, + "correlationId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "请求与最终交付的稳定关联 ID。" + } + }, + "additionalProperties": false + } + } + } + }, + { + "if": { + "properties": { + "group": { + "const": "user-handoff" + } + }, + "required": [ + "group" + ] + }, + "then": { + "properties": { + "audience": { + "type": "object", + "required": [ + "type", + "conversationId" + ], + "properties": { + "type": { + "type": "string", + "const": "user", + "description": "交付给当前用户可见会话。" + }, + "conversationId": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "用户接收交付的稳定会话 ID。" + } + }, + "additionalProperties": false + } + } + } + } + ], + "description": "本 Run 在准入时冻结的交付路由分组与真实受众。委派/异步恢复/interject 不得根据当前激活会话重新推断;旧 Run 可缺省并按受控兼容规则只读派生。" + }, + "subtask_inherited_from": { + "type": "object", + "description": "本 run 作为 subtask 子体时,继承的那份对话历史的来源坐标。与 resumedFrom 是**两条不同的边**:resumedFrom 是「同一委派任务的接力」(承接子 run 自己的历史),本字段是「继承发起方会话的上下文」(历史来自另一条时间线)。混用会让委派续跑的链校验把 subtask 的来源当成祖先去比对归属五元组,而两者的归属口径本就不同。与 resumedFrom 同一约定:继承来的历史只作为运行时上下文喂给模型,**不落进本 run 的 messages.jsonl**,因此这是事后追溯「这条 run 看过谁的上下文」的唯一依据。", + "properties": { + "conversation_id": { + "type": "string", + "description": "历史来源会话 ID(发起方有话题时用这条;跨 run 归并后的规范视图)" + }, + "run_id": { + "type": "string", + "description": "历史来源 run ID(发起方本身是委派子 run、不属于任何会话时用这条)" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "RunRuntimeState": { + "type": "object", + "required": [ + "run_id", + "agent_id", + "status" + ], + "properties": { + "run_id": { + "type": "string", + "description": "被查询的 Run ID;兼容普通 snake_case Run 与 checkpoint/rewind 产生的 camelCase fork Run" + }, + "agent_id": { + "type": "string", + "description": "该 Run 的权威 Agent 归属,用于审阅等破坏性操作在客户端预检时核对来源" + }, + "status": { + "type": "string", + "description": "Raw Run state. idle does not prove successful completion of a particular submitted turn.", + "enum": [ + "running", + "idle", + "completed", + "failed" + ] + } + }, + "additionalProperties": false + }, + "RunReceipts": { + "type": "object", + "required": [ + "run_id", + "receipts" + ], + "properties": { + "run_id": { + "type": "string", + "minLength": 1, + "description": "当前可信用户有权读取的 Run ID。" + }, + "receipts": { + "type": "array", + "items": { + "type": "object", + "required": [ + "run_id", + "goal", + "input", + "output", + "tool_calls", + "outcome", + "started_at", + "completed_at" + ], + "properties": { + "receipt_id": { + "type": "string", + "minLength": 1 + }, + "run_id": { + "type": "string", + "description": "关联的 Run ID" + }, + "execution_attachment": { + "type": "object", + "required": [ + "userId", + "agentId", + "runId" + ], + "properties": { + "userId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$" + }, + "agentId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "执行该 Run 的权威 Agent ID。" + }, + "runId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "承载本次执行的 DesireCore Run ID。" + }, + "conversationId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "可选的用户可见 Conversation ID;缺失表示该 Run 不属于对话时间线。" + }, + "workContextId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "可选的稳定 Work Context ID,用于跨续跑保持同一工作语境。" + }, + "workflowRunId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "可选的 Workflow Run ID;与 DesireCore Run ID 正交。" + }, + "cellId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "可选的执行 Cell ID,表示服务实例所在的逻辑隔离单元。" + }, + "serviceInstanceId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$", + "description": "可选的 Agent Service 实例 ID;出现时必须同时提供其所属 cellId。" + }, + "jobId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$" + }, + "attemptId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$" + }, + "capsuleId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^\\S+$" + } + }, + "allOf": [ + { + "if": { + "required": [ + "serviceInstanceId" + ] + }, + "then": { + "required": [ + "cellId" + ] + } + }, + { + "if": { + "required": [ + "attemptId" + ] + }, + "then": { + "required": [ + "jobId" + ] + } + }, + { + "if": { + "required": [ + "capsuleId" + ] + }, + "then": { + "required": [ + "jobId", + "attemptId" + ] + } + } + ], + "additionalProperties": false, + "description": "生成本回执的完整 ID-only 执行归属;旧回执可缺失。" + }, + "goal": { + "type": "string", + "description": "执行目标(从用户输入中提取的任务描述)" + }, + "input": { + "type": "string", + "description": "用户原始输入(完整的用户消息)" + }, + "output": { + "type": "string", + "description": "最终输出文本(Agent 的完整响应)" + }, + "tool_calls": { + "type": "array", + "description": "工具调用列表(按执行顺序排列)", + "items": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "invocation_id": { + "type": "string", + "maxLength": 256, + "description": "工具调用的全局唯一标识;用于在同名、重复或并行调用中准确关联开始与结束记录" + }, + "name": { + "type": "string", + "description": "工具名称(如 read_file、execute_command)" + }, + "input": { + "description": "工具输入审计摘要;新记录仅保存 param_keys,不保存参数值或凭据,旧回执可保留原结构" + }, + "output_summary": { + "type": "string", + "description": "工具输出摘要(用于快速了解工具执行结果,非完整输出)" + }, + "duration_ms": { + "type": "number", + "description": "执行耗时(毫秒),用于性能分析", + "minimum": 0 + }, + "started_at": { + "type": "string", + "description": "该工具调用开始时间(ISO 8601)", + "format": "date-time" + }, + "completed_at": { + "type": "string", + "description": "该工具调用结束时间(ISO 8601)", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "工具调用执行结果:success(成功)、error(执行出错)", + "enum": [ + "success", + "error" + ] + } + }, + "additionalProperties": false + } + }, + "evidence": { + "type": "string", + "description": "决策依据(来自 thinking 或 reasoning 过程,解释 Agent 为何做出某些决策)" + }, + "outcome": { + "type": "object", + "required": [ + "status", + "auto_completed" + ], + "properties": { + "status": { + "type": "string", + "description": "结果状态:success(成功完成)、partial(部分完成)、failed(执行失败)", + "enum": [ + "success", + "partial", + "failed" + ] + }, + "auto_completed": { + "type": "boolean", + "description": "是否无人工干预完成(true 表示完全自动执行,false 表示需要用户确认或修正)" + }, + "user_edit_count": { + "type": "number", + "minimum": 0 + }, + "partial_result_available": { + "type": "boolean", + "description": "partial 回执是否包含可直接展示的已完成阶段结果,而不是空占位" + }, + "pending_synthesis": { + "type": "boolean", + "description": "是否仍需基于已完成工具事实生成最终业务解释;恢复时不得因此重复执行成功工具" + } + }, + "additionalProperties": false + }, + "started_at": { + "type": "string", + "description": "开始时间(ISO 8601 格式)", + "format": "date-time" + }, + "completed_at": { + "type": "string", + "description": "完成时间(ISO 8601 格式)", + "format": "date-time" + }, + "duration_ms": { + "type": "number", + "description": "总耗时(毫秒),从开始到完成的完整时间", + "minimum": 0 + }, + "usage": { + "type": "object", + "properties": { + "input_tokens": { + "type": "number", + "description": "本次查询的非缓存输入 token 数(不含 cache_read / cache_write;per-query delta,不累计)", + "minimum": 0 + }, + "output_tokens": { + "type": "number", + "description": "本次查询的输出 token 数(per-query delta,不累计)", + "minimum": 0 + }, + "cost_usd": { + "type": "number", + "description": "本次查询的成本(美元,已包含缓存费用;per-query delta,不累计)", + "minimum": 0 + }, + "provider": { + "type": "string", + "description": "使用的提供商标识(如 openai、anthropic)" + }, + "model": { + "type": "string", + "description": "使用的模型 ID(如 gpt-5-mini、claude-sonnet-4)" + }, + "cache_read_tokens": { + "type": "number", + "description": "本次查询从 prompt 缓存读取的 token 数(per-query delta,不累计)", + "minimum": 0 + }, + "cache_write_tokens": { + "type": "number", + "description": "本次查询写入 prompt 缓存的 token 数(per-query delta,不累计)", + "minimum": 0 + } + }, + "additionalProperties": false + }, + "route_decision": { + "type": "object", + "additionalProperties": true + }, + "interruption_reason": { + "type": "string", + "description": "本条 partial 回执对应的主动中止原因。旧回执无此字段,保持向后兼容", + "enum": [ + "user_abort", + "agent_interrupt", + "reset", + "system_suspend", + "process_shutdown", + "process_restart", + "orphan_reclaimed", + "background_timeout" + ] + } + }, + "additionalProperties": false + }, + "description": "该 Run 的结构化回执,按写入顺序。" + }, + "total": { + "type": "integer", + "minimum": 0, + "description": "分页读取时该 Run 的有效 Receipt 总数。" + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "从最新 Receipt 向前跳过的数量。" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "本页最多返回的 Receipt 数。" + }, + "has_more": { + "type": "boolean", + "description": "是否仍有更早的 Receipt 可按需加载。" + }, + "skipped": { + "type": "integer", + "minimum": 0, + "description": "流式读取中跳过的损坏或超大行数。" + } + }, + "additionalProperties": false, + "description": "单个用户可见 Run 的 Receipt 响应。" + }, + "AgentList": { + "type": "object", + "required": [ + "agents" + ], + "properties": { + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentSummary" + } + } + }, + "additionalProperties": false + }, + "RunList": { + "type": "object", + "required": [ + "runs" + ], + "properties": { + "runs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RunMeta" + } + } + }, + "additionalProperties": false + }, + "OperationsError": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + }, + "additionalProperties": false + }, + "QueryStartRequest": { + "type": "object", + "anyOf": [ + { + "required": [ + "prompt" + ], + "properties": { + "prompt": { + "type": "string", + "minLength": 1 + } + } + }, + { + "required": [ + "mediaRefs" + ], + "properties": { + "mediaRefs": { + "type": "array", + "minItems": 1 + } + } + } + ], + "properties": { + "prompt": { + "type": "string", + "description": "当前用户提交的提示词。" + }, + "options": { + "type": "object", + "properties": { + "routingMode": { + "type": "string", + "enum": [ + "smart", + "fixed" + ], + "description": "模型路由模式。" + }, + "cwd": { + "type": "string", + "description": "当前轮授权工作目录。" + }, + "systemPrompt": { + "type": "string", + "description": "调用方附加的系统提示词。" + }, + "allowedTools": { + "type": "array", + "items": { + "type": "string" + }, + "description": "允许的工具 ID。" + }, + "disallowedTools": { + "type": "array", + "items": { + "type": "string" + }, + "description": "禁止的工具 ID。" + }, + "maxTurns": { + "type": "number", + "description": "最大 Agent 回合数。" + }, + "model": { + "type": "string", + "description": "模型名称。" + }, + "engine": { + "const": "desirecore-engine", + "description": "运行时引擎。" + }, + "provider": { + "type": "string", + "description": "Provider 协议族。" + }, + "providerId": { + "type": "string", + "description": "Provider 唯一 ID。" + }, + "apiKey": { + "type": "string", + "deprecated": true, + "description": "Compatibility-only provider credential override. Configure provider credentials on the instance; this field does not authenticate the HTTP request." + }, + "reasoning": { + "type": "string", + "description": "本轮 reasoning 模式。" + }, + "reasoningDefault": { + "type": "string" + }, + "suppressThinking": { + "type": "boolean", + "description": "仅隐藏公开思考展示,不改变模型思考强度或服务端私有协议回放。" + }, + "immersiveVoice": { + "type": "object", + "additionalProperties": false, + "required": [ + "modelSource", + "reasoning" + ], + "properties": { + "modelSource": { + "type": "string", + "enum": [ + "conversation", + "fast", + "specified" + ], + "description": "当前对话、低延迟默认映射或人类指定的精确模型;低延迟不代表低费用。" + }, + "providerId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "specified 时使用的本地 Provider 接入面 ID。" + }, + "modelName": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "description": "specified 时使用的精确模型 ID。" + }, + "reasoning": { + "type": "string", + "enum": [ + "follow", + "low-latency", + "auto", + "off", + "minimal", + "low", + "medium", + "high", + "xhigh", + "max" + ], + "description": "follow 沿用对话;low-latency 只选择模型显式支持的最低档位;其他值为独立思考强度,不受显示思考开关影响。" + } + } + }, + "voiceSelectionRef": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "agentId": { + "type": "string", + "description": "当前 Agent ID。" + }, + "conversationHistory": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueryStartRequest/definitions/conversationMessage" + }, + "description": "多轮连续性使用的结构化历史。" + }, + "conversationId": { + "type": "string", + "description": "对话稳定 ID。" + }, + "resumeFromDisk": { + "type": "boolean", + "description": "是否优先从磁盘恢复权威结构化历史。" + }, + "preferredRunId": { + "type": "string", + "description": "用户显式选择的恢复分支 run ID。" + }, + "excludeMessageIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "恢复时剔除的本轮重发消息 ID。" + }, + "channel": { + "type": "string", + "description": "消息来源渠道。" + }, + "skillIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "本轮显式选择的 Skill ID。" + }, + "skillArgs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "本轮 Skill 命令参数。" + }, + "fileReferences": { + "type": "array", + "items": { + "type": "string" + }, + "description": "用户引用的文件路径。" + }, + "activeDocPath": { + "type": "string", + "description": "本次请求的可选文件参考路径。自动采集仅来自本窗口、属于当前会话的可见文档;显式传入或重试沿用已冻结的引用,不按当下界面重选。读取仍须授权,引用本身不表示编辑或附件交付请求。" + }, + "file": { + "type": "object", + "required": [ + "name", + "size", + "type" + ], + "properties": { + "name": { + "type": "string", + "description": "附件名称。" + }, + "size": { + "type": "string", + "description": "附件展示大小。" + }, + "type": { + "type": "string", + "description": "附件类型。" + } + }, + "additionalProperties": false + }, + "teamId": { + "type": "string", + "description": "当前团队 ID。" + } + }, + "additionalProperties": false + }, + "agentReferences": { + "type": "array", + "items": { + "type": "object", + "required": [ + "occurrence_id", + "target", + "range" + ], + "properties": { + "occurrence_id": { + "type": "string", + "minLength": 8, + "maxLength": 256, + "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*$", + "description": "客户端生成、在一次来源 revision 内稳定的 occurrence ID;同一 Agent 可出现多次。" + }, + "target": { + "type": "object", + "required": [ + "agent_id", + "display_name_snapshot" + ], + "properties": { + "agent_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*$", + "description": "目标 Agent 的稳定语义 ID;仅允许 ASCII 字母、数字、下划线和连字符。" + }, + "display_name_snapshot": { + "type": "string", + "minLength": 1, + "description": "服务端按 agent_id 从可信 Agent Registry 重建的显示快照;客户端同名字段只用于提交时 range 校验,不参与身份或 system prompt。" + } + }, + "additionalProperties": false, + "description": "稳定 Agent 引用;是否执行由来源契约决定,显示名不能单独触发投递。" + }, + "range": { + "type": "object", + "required": [ + "start", + "end" + ], + "properties": { + "start": { + "type": "integer", + "minimum": 0, + "description": "正文 UTF-16 code unit 的起始偏移,包含。" + }, + "end": { + "type": "integer", + "minimum": 1, + "description": "正文 UTF-16 code unit 的结束偏移,不包含。" + } + }, + "additionalProperties": false, + "description": "结构化 token 在来源正文中的半开区间;服务端校验 start < end 且与正文匹配。" + } + }, + "additionalProperties": false, + "description": "picker 生成的结构化 Agent token;聊天中为非执行引用,看板中可由来源 owner 形成 Mention。" + }, + "description": "非执行 Agent 引用。" + }, + "mentionOccurrences": { + "type": "array", + "items": { + "type": "object", + "required": [ + "occurrence_id", + "target", + "range" + ], + "properties": { + "occurrence_id": { + "type": "string", + "minLength": 8, + "maxLength": 256, + "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*$", + "description": "客户端生成、在一次来源 revision 内稳定的 occurrence ID;同一 Agent 可出现多次。" + }, + "target": { + "type": "object", + "required": [ + "agent_id", + "display_name_snapshot" + ], + "properties": { + "agent_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*$", + "description": "目标 Agent 的稳定语义 ID;仅允许 ASCII 字母、数字、下划线和连字符。" + }, + "display_name_snapshot": { + "type": "string", + "minLength": 1, + "description": "服务端按 agent_id 从可信 Agent Registry 重建的显示快照;客户端同名字段只用于提交时 range 校验,不参与身份或 system prompt。" + } + }, + "additionalProperties": false, + "description": "稳定 Agent 引用;是否执行由来源契约决定,显示名不能单独触发投递。" + }, + "range": { + "type": "object", + "required": [ + "start", + "end" + ], + "properties": { + "start": { + "type": "integer", + "minimum": 0, + "description": "正文 UTF-16 code unit 的起始偏移,包含。" + }, + "end": { + "type": "integer", + "minimum": 1, + "description": "正文 UTF-16 code unit 的结束偏移,不包含。" + } + }, + "additionalProperties": false, + "description": "结构化 token 在来源正文中的半开区间;服务端校验 start < end 且与正文匹配。" + } + }, + "additionalProperties": false, + "description": "picker 生成的结构化 Agent token;聊天中为非执行引用,看板中可由来源 owner 形成 Mention。" + }, + "description": "Retired execution input; the HTTP route rejects this field. Use non-executable agentReferences.", + "deprecated": true + }, + "mentionRecipientRequests": { + "type": "array", + "items": { + "type": "object", + "required": [ + "occurrence_ids", + "recipient_agent_id", + "route" + ], + "properties": { + "occurrence_ids": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 8, + "maxLength": 256 + }, + "description": "同一来源 revision 中指向该 recipient 的全部 occurrence IDs;只产生一个 delivery。" + }, + "recipient_agent_id": { + "type": "string", + "minLength": 1, + "description": "必须与 occurrence.target.agent_id 一致的稳定 Agent ID。" + }, + "route": { + "type": "string", + "enum": [ + "current_conversation", + "external_agent" + ], + "description": "当前 Agent 走普通 conversation;非当前 Agent 走独立 Mention delivery。" + }, + "context_selection": { + "allOf": [ + { + "type": "object", + "required": [ + "context_mode", + "origin" + ], + "properties": { + "context_mode": { + "type": "string", + "enum": [ + "continue", + "isolated", + "fork" + ], + "description": "必填且无默认值:continue=继续明确存在的同一任务上下文;isolated=不同新任务;fork=同一任务有意分叉。" + }, + "origin": { + "type": "string", + "enum": [ + "delegate", + "mention", + "task", + "workflow", + "schedule", + "webhook", + "decision", + "legacy" + ] + }, + "work_context_id": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "description": "continue 必填:要继续的稳定 Work Context ID;禁止回退到目标 Agent 最近一次会话。" + }, + "source_work_context_id": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$", + "description": "fork 必填:有意分叉的来源 Work Context ID。" + }, + "task_ref": { + "type": "object", + "required": [ + "board_id", + "task_id" + ], + "properties": { + "board_id": { + "type": "string", + "minLength": 1 + }, + "task_id": { + "type": "string", + "minLength": 1, + "description": "TaskBoard 内稳定 Task ID。" + }, + "task_version": { + "type": "integer", + "minimum": 1, + "description": "选择时观察到的 Task 版本快照;身份查重只使用 board_id + task_id。" + } + }, + "additionalProperties": false + }, + "intent_id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "context_reason": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "description": "isolated/fork 必填:说明为何这是不同新任务或为何允许对同一 Task 有意分叉。" + } + }, + "allOf": [ + { + "if": { + "properties": { + "context_mode": { + "const": "continue" + } + }, + "required": [ + "context_mode" + ] + }, + "then": { + "required": [ + "work_context_id" + ], + "not": { + "anyOf": [ + { + "required": [ + "source_work_context_id" + ] + }, + { + "required": [ + "intent_id" + ] + } + ] + } + } + }, + { + "if": { + "properties": { + "context_mode": { + "const": "isolated" + } + }, + "required": [ + "context_mode" + ] + }, + "then": { + "required": [ + "context_reason" + ], + "anyOf": [ + { + "required": [ + "task_ref" + ] + }, + { + "required": [ + "intent_id" + ] + } + ], + "not": { + "anyOf": [ + { + "required": [ + "work_context_id" + ] + }, + { + "required": [ + "source_work_context_id" + ] + } + ] + } + } + }, + { + "if": { + "properties": { + "context_mode": { + "const": "fork" + } + }, + "required": [ + "context_mode" + ] + }, + "then": { + "required": [ + "source_work_context_id", + "task_ref", + "context_reason" + ], + "not": { + "anyOf": [ + { + "required": [ + "work_context_id" + ] + }, + { + "required": [ + "intent_id" + ] + } + ] + } + } + } + ], + "additionalProperties": false, + "description": "一次执行如何选择 Work Context;调用方必须显式提交,系统只能推荐不能代填。" + }, + { + "type": "object", + "required": [ + "origin" + ], + "properties": { + "origin": { + "type": "string", + "const": "mention", + "description": "Mention 外部投递只能创建 origin=mention 的 Work Context lineage。" + } + } + } + ], + "description": "非当前 recipient 单独提交的显式 Work Context selection;没有默认模式。" + }, + "retry_of_delivery_id": { + "type": "string", + "minLength": 1, + "description": "用户显式重新呼叫时引用旧 delivery;普通传输重试不得填写。" + } + }, + "allOf": [ + { + "if": { + "properties": { + "route": { + "const": "current_conversation" + } + }, + "required": [ + "route" + ] + }, + "then": { + "not": { + "anyOf": [ + { + "required": [ + "context_selection" + ] + }, + { + "required": [ + "retry_of_delivery_id" + ] + } + ] + } + } + }, + { + "if": { + "properties": { + "route": { + "const": "external_agent" + } + }, + "required": [ + "route" + ] + }, + "then": { + "required": [ + "context_selection" + ] + } + } + ], + "additionalProperties": false, + "description": "每个唯一 recipient 的确定性路由;多个显示 occurrence 共用一套显式 selection。" + }, + "description": "Retired execution input; the HTTP route rejects this field.", + "deprecated": true + }, + "mediaRefs": { + "type": "array", + "items": { + "type": "object", + "required": [ + "type", + "mediaId", + "name", + "mimeType", + "size" + ], + "properties": { + "type": { + "type": "string", + "description": "媒体业务类型。" + }, + "mediaId": { + "type": "string", + "description": "媒体存储引用。" + }, + "name": { + "type": "string", + "description": "原始文件名。" + }, + "mimeType": { + "type": "string", + "description": "媒体 MIME 类型。" + }, + "size": { + "type": "number", + "description": "媒体字节数。" + } + }, + "additionalProperties": true + }, + "description": "本轮媒体附件引用。" + }, + "images": { + "type": "array", + "items": { + "type": "object", + "required": [ + "data", + "mimeType" + ], + "properties": { + "data": { + "type": "string", + "description": "已废弃 Base64 图片数据。" + }, + "mimeType": { + "type": "string", + "description": "图片 MIME 类型。" + } + }, + "additionalProperties": false + }, + "description": "已废弃内联图片附件。" + }, + "clientRequestId": { + "type": "string", + "description": "客户端一次提交及其传输重试的稳定 ID。" + }, + "userMessageId": { + "type": "string" + }, + "requestDigest": { + "type": "string", + "maxLength": 128, + "description": "Optional digest produced by the shared request-body algorithm. The service recomputes it to reject reuse of a request ID with a changed business body." + }, + "originSocketId": { + "type": "string", + "description": "Current connected Socket.IO socket.id for precise event delivery; HTTP and Socket authentication must identify the same principal." + }, + "uiContextAdmission": { + "type": "object", + "required": [ + "version", + "admissionRef" + ], + "properties": { + "version": { + "type": "integer", + "const": 1, + "description": "Version of the durable UI context admission-reference envelope." + }, + "admissionRef": { + "type": "string", + "minLength": 56, + "maxLength": 56, + "pattern": "^ui_admission_[A-Za-z0-9_-]{43}$", + "description": "Opaque durable reference to an immutable admitted turn-context sidecar; it contains no snapshot, token, host identity, path, URL, or user text." + } + }, + "additionalProperties": false, + "description": "A valid context admission reference from an authorized UI flow. Ordinary standalone scripts omit it." + }, + "appWorkAdmissionRef": { + "type": "string", + "pattern": "^apa_[A-Za-z0-9_-]{43}$", + "maxLength": 512, + "description": "Opaque reference supplied by an authorized application proposal flow. Standalone integration examples omit it; an arbitrary reference does not grant authority." + } + }, + "definitions": { + "conversationContentBlock": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "text", + "tool_use", + "tool_result", + "image", + "thinking" + ], + "description": "结构化历史内容块类型。" + }, + "text": { + "type": "string", + "description": "文本块正文。" + }, + "id": { + "type": "string", + "description": "tool_use 块 ID。" + }, + "name": { + "type": "string", + "description": "工具名称。" + }, + "input": { + "description": "工具输入 JSON。" + }, + "tool_use_id": { + "type": "string", + "description": "tool_result 对应的 tool_use ID。" + }, + "content": { + "description": "工具结果文本或嵌套结构化内容块。", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueryStartRequest/definitions/conversationContentBlock" + } + } + ] + }, + "is_error": { + "type": "boolean", + "description": "工具结果是否表示错误。" + }, + "source": { + "type": "object", + "required": [ + "type", + "media_type", + "data" + ], + "properties": { + "type": { + "const": "base64", + "description": "图片数据编码类型。" + }, + "media_type": { + "type": "string", + "description": "图片 MIME 类型。" + }, + "data": { + "type": "string", + "description": "Base64 图片数据。" + } + }, + "additionalProperties": false + }, + "thinking": { + "type": "string", + "description": "Provider thinking 正文。" + } + }, + "additionalProperties": false, + "description": "Query conversationHistory 中的结构化内容块。" + }, + "conversationMessage": { + "type": "object", + "required": [ + "role", + "content" + ], + "properties": { + "role": { + "type": "string", + "enum": [ + "user", + "assistant", + "system" + ], + "description": "消息角色;system 仅用于受控上下文边界 marker。" + }, + "content": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueryStartRequest/definitions/conversationContentBlock" + }, + "description": "结构化消息内容。" + }, + "timestamp": { + "type": "number", + "description": "消息时间戳(毫秒)。" + }, + "id": { + "type": "string", + "description": "SessionMessage 空间中的稳定消息 ID。" + }, + "compactCutoff": { + "type": "boolean", + "description": "压缩摘要边界 marker。" + }, + "summarizedCount": { + "type": "number", + "description": "本次摘要覆盖的消息数。" + }, + "compactedAt": { + "type": "string", + "format": "date-time", + "description": "压缩发生时间。" + }, + "compactAnchorMessageId": { + "type": "string", + "description": "压缩后保留区第一条消息 ID。" + }, + "compactKeptTurns": { + "type": "number", + "description": "压缩后保留的对话轮数。" + }, + "compactCoverStartMessageId": { + "type": "string", + "description": "本 marker 覆盖区第一条消息 ID。" + }, + "compactRunId": { + "type": "string", + "description": "老 marker 的覆盖 run。" + }, + "compactMerged": { + "type": "boolean", + "description": "是否为摘要链二级合并产物。" + }, + "compactSupersedes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "被本 marker 取代的 marker ID 列表。" + }, + "compactTrigger": { + "type": "string", + "enum": [ + "auto", + "manual" + ], + "description": "压缩由自动阈值还是用户显式触发。" + }, + "compactWindow": { + "type": "number", + "description": "生成自动压缩 marker 时的有效上下文窗口。" + }, + "contextReset": { + "type": "boolean", + "description": "不携带摘要的显式遗忘边界。" + }, + "resetAnchorMessageId": { + "type": "string", + "description": "触发 reset 的 user 消息 ID。" + }, + "resetReason": { + "type": "string", + "description": "reset 的用户可见原因。" + }, + "fileReferences": { + "type": "array", + "items": { + "type": "string" + }, + "description": "用户引用的文件路径。" + }, + "mediaRefs": { + "type": "array", + "items": { + "type": "object", + "required": [ + "mediaId" + ], + "properties": { + "mediaId": { + "type": "string", + "description": "媒体存储中的稳定引用。" + }, + "mimeType": { + "type": "string", + "description": "媒体 MIME 类型。" + }, + "type": { + "type": "string", + "description": "媒体业务类型。" + } + }, + "additionalProperties": false + }, + "description": "历史消息引用的媒体。" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "not": { + "anyOf": [ + { + "required": [ + "mentionOccurrences" + ] + }, + { + "required": [ + "mentionRecipientRequests" + ] + } + ] + } + }, + "QueryAdmission": { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "boolean", + "description": "请求是否已被准入。" + }, + "clientRequestId": { + "type": "string", + "description": "对应的客户端请求 ID。" + }, + "duplicate": { + "type": "boolean", + "description": "是否命中幂等重复请求。" + }, + "busy": { + "type": "boolean", + "description": "准入被竞争挡下而非失败,仅两种来源:会话清除窗口(query:start、HTTP start、follow-up;清除结束后可用同一 clientRequestId 重试)与同一 session 已有 continue 在执行(仅 query:continue)。start 准入只有清除窗口会置位,客户端据此区分清除与其它 retryable 拒绝;缺省或 false 不代表清除。" + }, + "error": { + "type": "string", + "description": "拒绝时的稳定错误说明。" + }, + "code": { + "type": "string", + "description": "稳定拒绝码,如 CLIENT_REQUEST_BODY_MISMATCH(同 ID 不同请求体,不可重试)。" + }, + "retryable": { + "type": "boolean", + "description": "是否允许用同一 clientRequestId 重试。" + }, + "uiContextFaultCode": { + "type": "string", + "enum": [ + "snapshot_missing", + "snapshot_stale", + "surface_mismatch", + "auth_required", + "human_required", + "redaction_blocked", + "adapter_invalid", + "snapshot_conflict", + "protocol_unsupported", + "host_unavailable", + "action_unavailable", + "action_failed", + "action_outcome_unknown", + "action_capacity_exceeded" + ], + "description": "Stable fail-closed UI context protocol fault code." + }, + "sessionId": { + "type": "string", + "description": "已建立或已存在的 session ID。" + } + }, + "additionalProperties": false, + "description": "Socket.IO query:start 与 HTTP start 的准入响应。" + }, + "QueryFollowUpRequest": { + "type": "object", + "required": [ + "sessionId", + "originSocketId", + "message" + ], + "properties": { + "sessionId": { + "type": "string" + }, + "originSocketId": { + "type": "string", + "minLength": 1, + "description": "Current connected socket.id, authenticated as the same principal as the HTTP request." + }, + "message": { + "type": "object", + "required": [ + "role", + "content" + ], + "properties": { + "id": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "user", + "assistant" + ] + }, + "content": { + "type": "string" + }, + "timestamp": { + "type": "number" + } + }, + "additionalProperties": true + }, + "clientRequestId": { + "type": "string" + }, + "requestDigest": { + "type": "string" + }, + "mediaRefs": { + "type": "array", + "items": { + "type": "object", + "required": [ + "type", + "mediaId", + "name", + "mimeType", + "size" + ], + "properties": { + "type": { + "type": "string", + "description": "媒体业务类型。" + }, + "mediaId": { + "type": "string", + "description": "媒体存储引用。" + }, + "name": { + "type": "string", + "description": "原始文件名。" + }, + "mimeType": { + "type": "string", + "description": "媒体 MIME 类型。" + }, + "size": { + "type": "number", + "description": "媒体字节数。" + } + }, + "additionalProperties": true + }, + "description": "本轮媒体附件引用。" + }, + "images": { + "type": "array", + "items": { + "type": "object", + "required": [ + "data", + "mimeType" + ], + "properties": { + "data": { + "type": "string", + "description": "已废弃 Base64 图片数据。" + }, + "mimeType": { + "type": "string", + "description": "图片 MIME 类型。" + } + }, + "additionalProperties": false + }, + "description": "已废弃内联图片附件。" + }, + "fileReferences": { + "type": "array", + "items": { + "type": "string" + } + }, + "activeDocPath": { + "type": "string" + }, + "reasoning": { + "type": "string" + }, + "agentReferences": { + "type": "array", + "items": { + "type": "object", + "required": [ + "occurrence_id", + "target", + "range" + ], + "properties": { + "occurrence_id": { + "type": "string", + "minLength": 8, + "maxLength": 256, + "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*$", + "description": "客户端生成、在一次来源 revision 内稳定的 occurrence ID;同一 Agent 可出现多次。" + }, + "target": { + "type": "object", + "required": [ + "agent_id", + "display_name_snapshot" + ], + "properties": { + "agent_id": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*$", + "description": "目标 Agent 的稳定语义 ID;仅允许 ASCII 字母、数字、下划线和连字符。" + }, + "display_name_snapshot": { + "type": "string", + "minLength": 1, + "description": "服务端按 agent_id 从可信 Agent Registry 重建的显示快照;客户端同名字段只用于提交时 range 校验,不参与身份或 system prompt。" + } + }, + "additionalProperties": false, + "description": "稳定 Agent 引用;是否执行由来源契约决定,显示名不能单独触发投递。" + }, + "range": { + "type": "object", + "required": [ + "start", + "end" + ], + "properties": { + "start": { + "type": "integer", + "minimum": 0, + "description": "正文 UTF-16 code unit 的起始偏移,包含。" + }, + "end": { + "type": "integer", + "minimum": 1, + "description": "正文 UTF-16 code unit 的结束偏移,不包含。" + } + }, + "additionalProperties": false, + "description": "结构化 token 在来源正文中的半开区间;服务端校验 start < end 且与正文匹配。" + } + }, + "additionalProperties": false, + "description": "picker 生成的结构化 Agent token;聊天中为非执行引用,看板中可由来源 owner 形成 Mention。" + }, + "description": "非执行 Agent 引用。" + }, + "uiContextAdmission": { + "type": "object", + "required": [ + "version", + "admissionRef" + ], + "properties": { + "version": { + "type": "integer", + "const": 1, + "description": "Version of the durable UI context admission-reference envelope." + }, + "admissionRef": { + "type": "string", + "minLength": 56, + "maxLength": 56, + "pattern": "^ui_admission_[A-Za-z0-9_-]{43}$", + "description": "Opaque durable reference to an immutable admitted turn-context sidecar; it contains no snapshot, token, host identity, path, URL, or user text." + } + }, + "additionalProperties": false, + "description": "Only UI context object allowed on an actual query or pending record; omitting the entire field preserves the legacy no-context path." + } + }, + "additionalProperties": true, + "description": "Documented request subset from the route interface. Additional fields do not imply authorization or guaranteed support.", + "not": { + "anyOf": [ + { + "required": [ + "mentionOccurrences" + ] + }, + { + "required": [ + "mentionRecipientRequests" + ] + } + ] + } + }, + "ClientRequestLookupRequest": { + "type": "object", + "additionalProperties": false, + "required": [ + "agentId", + "requests" + ], + "description": "按 clientRequestId 只读查询服务端接纳状态;不占位、不清理、不产生任何副作用。", + "properties": { + "agentId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "\\S" + }, + "conversationId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "\\S" + }, + "requests": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "description": "要查询的提交列表,每项一个 clientRequestId。", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "clientRequestId" + ], + "properties": { + "clientRequestId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "\\S", + "description": "客户端一次提交及其全部重发共用的稳定 ID。" + }, + "userMessageId": { + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "\\S" + } + } + } + } + } + }, + "ClientRequestLookupResponse": { + "type": "object", + "additionalProperties": false, + "required": [ + "protocolVersion", + "serverInstanceId", + "observedAt", + "autoReplayRetentionMs", + "tombstoneRetentionMs", + "results" + ], + "description": "只读接纳查询结果。", + "properties": { + "protocolVersion": { + "type": "integer", + "const": 1, + "description": "查询协议版本。" + }, + "serverInstanceId": { + "type": "string", + "description": "当前 Agent Service 进程代际 ID;与恢复快照同源,重启即变化。" + }, + "observedAt": { + "type": "string", + "format": "date-time", + "description": "服务端观察时刻(ISO 8601),客户端用于抵御本地墙钟回拨。" + }, + "autoReplayRetentionMs": { + "type": "integer", + "minimum": 0, + "description": "客户端同 ID 自动重放窗口(毫秒)。" + }, + "tombstoneRetentionMs": { + "type": "integer", + "minimum": 0, + "description": "服务端幂等 tombstone 保护窗口(毫秒);超出后查询结论不可信。" + }, + "results": { + "type": "array", + "description": "与请求一一对应(按 clientRequestId)。", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "clientRequestId", + "status" + ], + "properties": { + "clientRequestId": { + "type": "string", + "description": "被查询的 clientRequestId。" + }, + "status": { + "type": "string", + "enum": [ + "committed", + "reserved", + "absent", + "unknown" + ], + "description": "committed=已持久提交(至多一次,原 ID 不会再执行);reserved=当前服务进程内占位中(排队或正在做提交前复检);absent=截至此刻既无提交也无占位(原请求可能仍在路上,必须同 ID 重发由服务端裁决);unknown=无法判定(旧版无作用域记录、存储不可读或注册表 fail closed)。" + }, + "committedAt": { + "type": "string", + "format": "date-time", + "description": "status=committed 时的持久提交时刻。" + }, + "sessionId": { + "type": "string" + }, + "committedServerInstance": { + "type": "string", + "enum": [ + "current", + "previous", + "unknown" + ] + }, + "executionWitness": { + "type": "string", + "enum": [ + "user-message", + "none", + "unknown" + ], + "description": "user-message=磁盘已有该提交的用户消息(执行过);none=没有;unknown=无法查证(缺 userMessageId 或存储不可读)。" + } + } + } + } + } + }, + "WebhookManagement": { + "type": "object", + "required": [ + "enabled", + "hasSecret", + "events", + "availableTools" + ], + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "hasSecret": { + "type": "boolean", + "description": "是否已有非空 secret;读取不返回其值。" + }, + "events": { + "type": "array", + "items": { + "type": "object", + "required": [ + "event", + "promptTemplate", + "maxTurns", + "toolIds" + ], + "additionalProperties": false, + "properties": { + "event": { + "type": "string" + }, + "promptTemplate": { + "type": "string" + }, + "maxTurns": { + "type": "number" + }, + "toolIds": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "availableTools": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "name", + "riskLevel", + "requiresConfirmation" + ], + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "riskLevel": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ] + }, + "requiresConfirmation": { + "type": "boolean" + } + } + } + } + } + }, + "WebhookEventWrite": { + "type": "object", + "required": [ + "promptTemplate", + "toolIds" + ], + "properties": { + "promptTemplate": { + "type": "string", + "minLength": 1, + "maxLength": 32768, + "pattern": "\\S", + "description": "事件提示模板,{{key}} 从 payload 的顶层字段取值。" + }, + "toolIds": { + "type": "array", + "maxItems": 512, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "description": "从管理读取返回的 availableTools 中选择工具 ID;空数组表示不授权任何工具。" + }, + "maxTurns": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "事件最多执行的模型轮次数,省略时为 5。", + "default": 5 + } + }, + "additionalProperties": false + }, + "WebhookEventWritten": { + "type": "object", + "required": [ + "success", + "event", + "enabled" + ], + "additionalProperties": false, + "properties": { + "success": { + "const": true + }, + "event": { + "type": "string" + }, + "enabled": { + "const": true + }, + "secret": { + "type": "string", + "description": "仅在本次首次生成 secret 时返回;保存修改已有事件不会再次返回。" + } + } + }, + "WebhookEnabledRequest": { + "type": "object", + "required": [ + "enabled" + ], + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "开启或暂停该智能体已有 Webhook 事件。" + } + } + }, + "WebhookEnabledResponse": { + "type": "object", + "required": [ + "success", + "enabled" + ], + "additionalProperties": false, + "properties": { + "success": { + "const": true + }, + "enabled": { + "type": "boolean" + } + } + }, + "WebhookSecretRotation": { + "type": "object", + "required": [ + "secret" + ], + "additionalProperties": false, + "properties": { + "secret": { + "type": "string", + "minLength": 43, + "maxLength": 43 + } + } + }, + "ManagementError": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "details": { + "type": "array", + "items": {} + } + }, + "additionalProperties": false + }, + "ScheduleMessage": { + "type": "object", + "additionalProperties": false, + "description": "完整的可见调度消息契约;授权快照、正文、附件引用和收件人均接受运行时验证。", + "required": [ + "id", + "scheduleId", + "authorizationScope", + "text", + "isError", + "timestamp" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "可信结果路由给出的稳定、非授权标识。" + }, + "recipientUserId": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "description": "可信结果路由给出的稳定、非授权标识。" + }, + "origin": { + "enum": [ + "schedule", + "webhook" + ], + "description": "旧记录未声明时按 schedule 显示。" + }, + "scheduleId": { + "type": "string", + "description": "所属调度标识;不是执行授权。" + }, + "occurrenceId": { + "type": "string", + "description": "结果所属稳定发生标识,不能单凭它重新执行。" + }, + "authorizationScope": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind" + ], + "properties": { + "kind": { + "const": "agent", + "description": "Agent 作用域;结果还需精确收件人校验。" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", + "boardId", + "bindingId" + ], + "properties": { + "kind": { + "const": "taskboard", + "description": "保持原 TaskBoard 来源 ACL,不能退化为普通 Agent 作用域。" + }, + "boardId": { + "type": "string", + "description": "来源 TaskBoard 标识。" + }, + "bindingId": { + "type": "string", + "description": "冻结的资源绑定标识。" + } + } + } + ], + "description": "不可变来源授权快照;读取和清除都重新核验当前权限。" + }, + "displayName": { + "type": "string", + "description": "可选任务显示名。" + }, + "text": { + "type": "string", + "description": "受存储边界截断的可见摘要,不是执行指令。" + }, + "isError": { + "type": "boolean", + "description": "是否为失败结果;不会改变脚本真实终态。" + }, + "timestamp": { + "type": "number", + "description": "冻结的原交付时间,重放不得使用当前时间替换。" + }, + "delivery": { + "type": "object", + "additionalProperties": false, + "required": [ + "message", + "status", + "sentAt" + ], + "description": "既有 SendUserMessage 用户交付形状,仅保存附件引用。", + "properties": { + "message": { + "type": "string", + "description": "受存储边界截断的用户交付正文。" + }, + "status": { + "enum": [ + "normal", + "proactive" + ], + "description": "普通回复或主动告知。" + }, + "sentAt": { + "type": "string", + "description": "工具侧原交付时刻。" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + }, + "description": "附件解析降级告警。" + }, + "attachments": { + "type": "array", + "description": "引用型附件,不存二进制正文。", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "path", + "name", + "mimeType", + "size", + "isImage" + ], + "properties": { + "path": { + "type": "string", + "description": "已解析的附件路径,读取仍受资源准入限制。" + }, + "name": { + "type": "string", + "description": "附件文件名。" + }, + "mimeType": { + "type": "string", + "description": "附件 MIME 类型。" + }, + "size": { + "type": "number", + "description": "附件字节数。" + }, + "isImage": { + "type": "boolean", + "description": "是否为图片。" + }, + "mediaId": { + "type": "string", + "description": "可选媒体存储引用。" + }, + "width": { + "type": "number", + "description": "可选图片宽度。" + }, + "height": { + "type": "number", + "description": "可选图片高度。" + } + } + } + } + } + } + } + }, + "ScheduleMessagePage": { + "type": "object", + "required": [ + "items", + "total", + "limit", + "offset" + ], + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScheduleMessage" + } + }, + "total": { + "type": "integer", + "minimum": 0 + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 200 + }, + "offset": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false + } + } + }, + "x-service": "agent-service", + "x-source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed" + } +} diff --git a/static/openapi/compat-proxy.json b/static/openapi/compat-proxy.json new file mode 100644 index 00000000..e0a64b41 --- /dev/null +++ b/static/openapi/compat-proxy.json @@ -0,0 +1,934 @@ +{ + "openapi": "3.1.0", + "jsonSchemaDialect": "http://json-schema.org/draft-07/schema#", + "x-service": "compat-proxy", + "info": { + "title": "DesireCore Compat Proxy — public protocol subset", + "version": "1.0.0", + "description": "Three registered model protocol endpoints for trusted programs on the same machine. Dynamic loopback address and Provider-bound desirecore_ safe token are obtained through the documented credential flow. Protocol conversion does not guarantee all vendor features." + }, + "servers": [ + { + "url": "http://127.0.0.1:{port}", + "description": "Current loopback proxy origin. Obtain the actual Base URL including /v1, then use its origin here; do not duplicate /v1.", + "variables": { + "port": { + "default": "REPLACE_WITH_CURRENT_PORT" + } + } + } + ], + "tags": [ + { + "name": "Models", + "description": "Configured model metadata; listing is not filtered by the safe-token Provider binding." + }, + { + "name": "Protocol requests", + "description": "Messages and Chat Completions forwarding/conversion with backend-specific restrictions." + } + ], + "components": { + "securitySchemes": { + "ProxyBearer": { + "type": "http", + "scheme": "bearer", + "description": "Provider-bound desirecore_ safe token; not a JWT, Agent Service token or upstream provider key." + }, + "ProxyApiKey": { + "type": "apiKey", + "in": "header", + "name": "x-api-key", + "description": "The same Provider-bound safe token sent in x-api-key. A nonempty x-api-key takes precedence over Bearer when both are supplied." + } + }, + "schemas": { + "Models": { + "type": "object", + "properties": { + "object": { + "type": "string", + "const": "list" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "const": "model" + }, + "created": { + "type": "integer" + }, + "owned_by": { + "type": "string" + } + }, + "required": [ + "id", + "object", + "created", + "owned_by" + ], + "additionalProperties": true + } + } + }, + "required": [ + "object", + "data" + ], + "additionalProperties": true, + "x-schema-origin": "Protocol subset and actual proxy response implementation; not an exhaustive upstream protocol guarantee." + }, + "ProxyAuthenticationError": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "error" + }, + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication_error" + }, + "message": { + "type": "string" + } + }, + "required": [ + "type", + "message" + ], + "additionalProperties": true + } + }, + "required": [ + "type", + "error" + ], + "additionalProperties": true, + "x-schema-origin": "Protocol subset and actual proxy response implementation; not an exhaustive upstream protocol guarantee." + }, + "MessagesError": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "error" + }, + "error": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "type", + "message" + ], + "additionalProperties": true + } + }, + "required": [ + "type", + "error" + ], + "additionalProperties": true, + "x-schema-origin": "Protocol subset and actual proxy response implementation; not an exhaustive upstream protocol guarantee." + }, + "ChatCompletionError": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "required": [ + "message", + "type" + ], + "additionalProperties": true + } + }, + "required": [ + "error" + ], + "additionalProperties": true, + "x-schema-origin": "Protocol subset and actual proxy response implementation; not an exhaustive upstream protocol guarantee." + }, + "FrameworkError": { + "type": "object", + "properties": { + "statusCode": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "statusCode", + "error", + "message" + ], + "additionalProperties": true, + "x-schema-origin": "Protocol subset and actual proxy response implementation; not an exhaustive upstream protocol guarantee." + }, + "MessagesRequest": { + "type": "object", + "required": [ + "model", + "messages", + "max_tokens" + ], + "properties": { + "model": { + "type": "string", + "minLength": 1 + }, + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "user", + "assistant" + ] + }, + "content": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + ] + } + }, + "required": [ + "role", + "content" + ], + "additionalProperties": true + } + }, + "max_tokens": { + "type": "integer", + "minimum": 1 + }, + "system": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + ] + }, + "stream": { + "type": "boolean", + "default": false + }, + "temperature": { + "type": "number" + }, + "top_p": { + "type": "number" + }, + "stop_sequences": { + "type": "array", + "items": { + "type": "string" + } + }, + "tools": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "tool_choice": { + "type": "object", + "additionalProperties": true + }, + "thinking": { + "type": "object", + "additionalProperties": true + }, + "output_config": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true, + "description": "Documented Messages protocol subset. Backend restrictions and format conversion still apply; the proxy does not validate this as a closed request Schema.", + "x-schema-origin": "Protocol subset and actual proxy response implementation; not an exhaustive upstream protocol guarantee." + }, + "ChatCompletionRequest": { + "type": "object", + "required": [ + "model", + "messages" + ], + "properties": { + "model": { + "type": "string", + "minLength": 1 + }, + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string" + }, + "content": { + "type": [ + "string", + "array", + "null" + ] + } + }, + "required": [ + "role", + "content" + ], + "additionalProperties": true + } + }, + "max_completion_tokens": { + "type": "integer", + "minimum": 1 + }, + "max_tokens": { + "type": "integer", + "minimum": 1 + }, + "stream": { + "type": "boolean", + "default": false + }, + "stream_options": { + "type": "object", + "additionalProperties": true + }, + "temperature": { + "type": "number" + }, + "top_p": { + "type": "number" + }, + "stop": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "tools": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "tool_choice": {}, + "parallel_tool_calls": { + "type": "boolean" + }, + "reasoning_effort": { + "type": "string" + }, + "response_format": { + "type": "object", + "additionalProperties": true + } + }, + "additionalProperties": true, + "description": "Documented Chat Completions subset, not an exhaustive vendor request validator. Backend compatibility and authorization are checked during routing.", + "x-schema-origin": "Protocol subset and actual proxy response implementation; not an exhaustive upstream protocol guarantee." + }, + "MessagesResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "const": "message" + }, + "role": { + "type": "string", + "const": "assistant" + }, + "content": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + }, + "model": { + "type": "string" + }, + "stop_reason": { + "type": [ + "string", + "null" + ] + }, + "stop_sequence": { + "type": [ + "string", + "null" + ] + }, + "usage": { + "type": "object", + "properties": { + "input_tokens": { + "type": "number" + }, + "output_tokens": { + "type": "number" + } + }, + "additionalProperties": true + } + }, + "required": [ + "id", + "type", + "role", + "content", + "model", + "stop_reason", + "stop_sequence" + ], + "additionalProperties": true, + "x-schema-origin": "Protocol subset and actual proxy response implementation; not an exhaustive upstream protocol guarantee." + }, + "ChatCompletionResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "object": { + "type": "string", + "const": "chat.completion" + }, + "created": { + "type": "integer" + }, + "model": { + "type": "string" + }, + "choices": { + "type": "array", + "items": { + "type": "object", + "properties": { + "index": { + "type": "integer" + }, + "message": { + "type": "object", + "properties": { + "role": { + "type": "string" + }, + "content": { + "type": [ + "string", + "null" + ] + }, + "tool_calls": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": true + } + } + }, + "additionalProperties": true + }, + "finish_reason": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "index", + "message", + "finish_reason" + ], + "additionalProperties": true + } + }, + "usage": { + "type": "object", + "properties": { + "prompt_tokens": { + "type": "number" + }, + "completion_tokens": { + "type": "number" + }, + "total_tokens": { + "type": "number" + } + }, + "additionalProperties": true + } + }, + "required": [ + "id", + "object", + "created", + "model", + "choices" + ], + "additionalProperties": true, + "x-schema-origin": "Protocol subset and actual proxy response implementation; not an exhaustive upstream protocol guarantee." + } + } + }, + "paths": { + "/v1/models": { + "get": { + "operationId": "listProxyModels", + "tags": [ + "Models" + ], + "summary": "List configured models from all enabled Providers", + "description": "No pagination. The returned inventory is not filtered by the safe token Provider. Appearance in the list does not establish token authorization, model availability, or model release date; created is the listing time.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/model-proxy#proxy-models" + }, + "security": [ + { + "ProxyBearer": [] + }, + { + "ProxyApiKey": [] + } + ], + "responses": { + "200": { + "description": "Configured model inventory.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Models" + }, + "example": { + "object": "list", + "data": [ + { + "id": "your-model-name", + "object": "model", + "created": 1791943200, + "owned_by": "openai" + } + ] + } + } + } + }, + "401": { + "description": "Authentication failed; this Messages-shaped error is used for both protocols.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProxyAuthenticationError" + }, + "example": { + "type": "error", + "error": { + "type": "authentication_error", + "message": "Unauthorized compat-proxy request" + } + } + } + } + }, + "default": { + "description": "Other backend or reverse-proxy responses. Error formats vary; retain status, Content-Type and body.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/v1/messages": { + "post": { + "operationId": "createProxyMessage", + "tags": [ + "Protocol requests" + ], + "summary": "Request a Messages completion", + "description": "Requires an exact modelName served by the token-bound Provider. A supplied x-desirecore-provider-id cannot change this binding. stream=true returns protocol-specific SSE; headers or partial text do not establish successful completion. The client executes any authorized tool calls itself.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/model-proxy#proxy-messages" + }, + "security": [ + { + "ProxyBearer": [] + }, + { + "ProxyApiKey": [] + } + ], + "parameters": [ + { + "name": "anthropic-version", + "in": "header", + "required": false, + "schema": { + "type": "string", + "const": "2023-06-01" + }, + "description": "Stable Messages version; unsupported beta semantics are rejected for external safe-token callers." + } + ], + "x-max-body-bytes": 268435456, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessagesRequest" + }, + "example": { + "model": "your-model-name", + "max_tokens": 64, + "stream": false, + "messages": [ + { + "role": "user", + "content": "Reply only with OK." + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Non-streaming protocol response or streaming SSE. Read stop/finish reason and errors, including errors after headers.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessagesResponse" + }, + "example": { + "id": "msg_example", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "OK" + } + ], + "model": "your-model-name", + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 6, + "output_tokens": 1 + } + } + }, + "text/event-stream": { + "schema": { + "type": "string", + "description": "Protocol SSE stream, whose structure depends on the protocol and backend." + } + } + } + }, + "400": { + "description": "Missing model, invalid route or unsupported protocol input. Upstream errors may use another format.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessagesError" + } + } + } + }, + "401": { + "description": "Authentication failed; this Messages-shaped error is used for both protocols.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProxyAuthenticationError" + }, + "example": { + "type": "error", + "error": { + "type": "authentication_error", + "message": "Unauthorized compat-proxy request" + } + } + } + } + }, + "413": { + "description": "Request body exceeds 256 MiB.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FrameworkError" + } + } + } + }, + "415": { + "description": "Use Content-Type: application/json.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FrameworkError" + } + } + } + }, + "default": { + "description": "Other backend or reverse-proxy responses. Error formats vary; retain status, Content-Type and body.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/v1/chat/completions": { + "post": { + "operationId": "createProxyChatCompletion", + "tags": [ + "Protocol requests" + ], + "summary": "Request a Chat Completions completion", + "description": "Requires an exact modelName served by the token-bound Provider. A supplied x-desirecore-provider-id cannot change this binding. stream=true returns protocol-specific SSE; headers or partial text do not establish successful completion. The client executes any authorized tool calls itself.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/model-proxy#proxy-chat-completions" + }, + "security": [ + { + "ProxyBearer": [] + }, + { + "ProxyApiKey": [] + } + ], + "parameters": [], + "x-max-body-bytes": 268435456, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatCompletionRequest" + }, + "example": { + "model": "your-model-name", + "max_completion_tokens": 64, + "stream": false, + "messages": [ + { + "role": "user", + "content": "Reply only with OK." + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Non-streaming protocol response or streaming SSE. Read stop/finish reason and errors, including errors after headers.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatCompletionResponse" + }, + "example": { + "id": "chatcmpl_example", + "object": "chat.completion", + "created": 1791943200, + "model": "your-model-name", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "OK" + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 6, + "completion_tokens": 1, + "total_tokens": 7 + } + } + }, + "text/event-stream": { + "schema": { + "type": "string", + "description": "Protocol SSE stream, whose structure depends on the protocol and backend." + } + } + } + }, + "400": { + "description": "Missing model, invalid route or unsupported protocol input. Upstream errors may use another format.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChatCompletionError" + } + } + } + }, + "401": { + "description": "Authentication failed; this Messages-shaped error is used for both protocols.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProxyAuthenticationError" + }, + "example": { + "type": "error", + "error": { + "type": "authentication_error", + "message": "Unauthorized compat-proxy request" + } + } + } + } + }, + "413": { + "description": "Request body exceeds 256 MiB.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FrameworkError" + } + } + } + }, + "415": { + "description": "Use Content-Type: application/json.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FrameworkError" + } + } + } + }, + "default": { + "description": "Other backend or reverse-proxy responses. Error formats vary; retain status, Content-Type and body.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "x-source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed" + } +} diff --git a/static/openapi/workflow-service.json b/static/openapi/workflow-service.json new file mode 100644 index 00000000..b128f1e7 --- /dev/null +++ b/static/openapi/workflow-service.json @@ -0,0 +1,952 @@ +{ + "openapi": "3.1.0", + "jsonSchemaDialect": "http://json-schema.org/draft-07/schema#", + "x-service": "workflow-service", + "info": { + "title": "DesireCore Workflow Service — public integration subset", + "version": "1.0.0", + "description": "Independent administrator-provided Workflow Service origin and service credential. Only administrator-approved global Webhook definitions and read/observe endpoints are covered. Response Schemas manually present the current TypeScript/route contract; they do not claim a closed runtime validator. Execution-grant /run and retired definition writes are not covered." + }, + "servers": [ + { + "url": "https://{workflowHost}", + "description": "Use the independently provided Workflow Service origin. Default Agent Service Caddy deployment rejects these workflow paths.", + "variables": { + "workflowHost": { + "default": "workflow.example.com" + } + } + } + ], + "tags": [ + { + "name": "Workflow integration", + "description": "Service-authorized trigger, live events and historical reads for trusted administrators." + } + ], + "components": { + "securitySchemes": { + "WorkflowToken": { + "type": "apiKey", + "in": "header", + "name": "X-Workflow-Service-Token", + "description": "Existing WORKFLOW_SERVICE_TOKEN supplied by the workflow deployment administrator. Separate service boundary even if a deployment reuses AGENT_SERVICE_TOKEN." + } + }, + "schemas": { + "WorkflowError": { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "additionalProperties": true, + "x-schema-origin": "Manual presentation of workflow-contracts TypeScript interfaces and current route output; no closed runtime HTTP Schema is asserted." + }, + "WorkflowParameters": { + "type": "object", + "additionalProperties": true, + "description": "Workflow-specific input fields passed directly as params. The HTTP handler has no closed request Schema; omit the body for empty parameters.", + "x-schema-origin": "Manual presentation of workflow-contracts TypeScript interfaces and current route output; no closed runtime HTTP Schema is asserted." + }, + "WorkflowWebhookAccepted": { + "type": "object", + "properties": { + "runId": { + "type": "string" + }, + "workflowId": { + "type": "string" + }, + "status": { + "type": "string", + "const": "started" + } + }, + "required": [ + "runId", + "workflowId", + "status" + ], + "additionalProperties": true, + "x-schema-origin": "Manual presentation of workflow-contracts TypeScript interfaces and current route output; no closed runtime HTTP Schema is asserted." + }, + "WorkflowRunSummary": { + "type": "object", + "required": [ + "runId", + "status", + "startedAt", + "nodeCount", + "completedNodeCount", + "taskBoardBound" + ], + "properties": { + "runId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "totalDurationMs": { + "type": "number" + }, + "error": { + "type": "string" + }, + "taskBoardBound": { + "type": "boolean" + }, + "taskTrigger": { + "type": "object", + "additionalProperties": true + }, + "taskBoardScope": { + "type": "object", + "additionalProperties": true + }, + "principalActor": { + "type": "object", + "additionalProperties": true + }, + "executionIdentity": { + "type": "object", + "additionalProperties": true + }, + "decisionTreeOrigin": { + "type": "object", + "additionalProperties": true + }, + "nodeCount": { + "type": "number" + }, + "completedNodeCount": { + "type": "number" + } + }, + "additionalProperties": true, + "x-schema-origin": "Manual presentation of workflow-contracts TypeScript interfaces and current route output; no closed runtime HTTP Schema is asserted." + }, + "WorkflowRunDetail": { + "type": "object", + "required": [ + "workflowId", + "runId", + "status", + "startedAt", + "taskBoardBound" + ], + "properties": { + "workflowId": { + "type": "string" + }, + "runId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "startedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + }, + "totalDurationMs": { + "type": "number" + }, + "error": { + "type": "string" + }, + "taskBoardBound": { + "type": "boolean" + }, + "taskTrigger": { + "type": "object", + "additionalProperties": true + }, + "taskBoardScope": { + "type": "object", + "additionalProperties": true + }, + "principalActor": { + "type": "object", + "additionalProperties": true + }, + "executionIdentity": { + "type": "object", + "additionalProperties": true + }, + "decisionTreeOrigin": { + "type": "object", + "additionalProperties": true + }, + "nodeResults": { + "type": "object", + "additionalProperties": true + }, + "finalOutputs": { + "type": "object", + "additionalProperties": true + }, + "humanGate": { + "type": "object", + "properties": { + "nodeId": { + "type": "string" + }, + "prompt": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "label", + "value" + ], + "additionalProperties": true + } + } + }, + "required": [ + "nodeId", + "prompt", + "options" + ], + "additionalProperties": true + } + }, + "additionalProperties": true, + "x-schema-origin": "Manual presentation of workflow-contracts TypeScript interfaces and current route output; no closed runtime HTTP Schema is asserted." + }, + "WorkflowList": { + "type": "object", + "properties": { + "workflows": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id", + "name", + "description", + "creator", + "nodeCount", + "dslPath" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "creator": { + "type": "string" + }, + "nodeCount": { + "type": "number" + }, + "dslPath": { + "type": "string" + }, + "lastRun": { + "type": "object", + "required": [ + "status", + "taskBoardBound" + ], + "properties": { + "status": { + "type": "string" + }, + "taskBoardBound": { + "type": "boolean" + }, + "totalDurationMs": { + "type": "number" + }, + "completedAt": { + "type": "string" + } + }, + "additionalProperties": true + } + }, + "additionalProperties": true + } + } + }, + "required": [ + "workflows" + ], + "additionalProperties": true, + "x-schema-origin": "Manual presentation of workflow-contracts TypeScript interfaces and current route output; no closed runtime HTTP Schema is asserted." + }, + "WorkflowRunList": { + "type": "object", + "properties": { + "runs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkflowRunSummary" + } + } + }, + "required": [ + "runs" + ], + "additionalProperties": true, + "x-schema-origin": "Manual presentation of workflow-contracts TypeScript interfaces and current route output; no closed runtime HTTP Schema is asserted." + } + } + }, + "paths": { + "/api/workflow/webhook": { + "post": { + "operationId": "triggerWorkflowWebhook", + "tags": [ + "Workflow integration" + ], + "summary": "Trigger an administrator-approved global workflow", + "description": "The entire JSON body is the workflow-specific params object. Returns an asynchronous runId with status started; later execution may fail. No idempotency or request deduplication is provided; a lost response must not cause an unconditional retrigger.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/workflow-integration" + }, + "security": [ + { + "WorkflowToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "query", + "required": true, + "schema": { + "type": "string", + "minLength": 1 + } + } + ], + "responses": { + "200": { + "description": "The workflow run was accepted, not completed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowWebhookAccepted" + }, + "example": { + "runId": "00000000-0000-4000-8000-000000000010", + "workflowId": "your-workflow-id", + "status": "started" + } + } + } + }, + "400": { + "description": "The id query parameter is missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "401": { + "description": "Missing or invalid standalone Workflow Service credential.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "403": { + "description": "Browser-origin or resource/TaskBoard scope authorization failed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "404": { + "description": "The requested global workflow cannot be resolved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "503": { + "description": "Service credential is not initialized, the owner is fenced, or a run could not be reserved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "default": { + "description": "Other backend or reverse-proxy responses. Error formats vary; retain status, Content-Type and body.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + }, + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowParameters" + }, + "example": { + "name": "API" + } + } + } + } + } + }, + "/api/workflow/events": { + "get": { + "operationId": "watchWorkflowRunEvents", + "tags": [ + "Workflow integration" + ], + "summary": "Watch live Workflow events over SSE", + "description": "Send the service token in a header, never in a query string. One active subscriber per run. This service-level gate is for trusted administrators, not a per-user Run ACL. Current-process terminal runs may return only workflow:complete with a failed or other terminal status; historical streams are not durable full-event replay. Read run history after a disconnect.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/workflow-integration" + }, + "security": [ + { + "WorkflowToken": [] + } + ], + "parameters": [ + { + "name": "runId", + "in": "query", + "required": true, + "schema": { + "type": "string", + "minLength": 1 + } + } + ], + "responses": { + "200": { + "description": "Named SSE events with JSON data. Inspect workflow:complete status or workflow:error; the event name alone is not success.", + "headers": { + "Cache-Control": { + "schema": { + "type": "string", + "const": "no-cache" + } + } + }, + "content": { + "text/event-stream": { + "schema": { + "type": "string" + }, + "example": "event: workflow:complete\ndata: {\"runId\":\"example-run-id\",\"status\":\"completed\"}\n\n" + } + } + }, + "400": { + "description": "runId is missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "401": { + "description": "Missing or invalid standalone Workflow Service credential.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "403": { + "description": "Browser-origin or resource/TaskBoard scope authorization failed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "404": { + "description": "No current-process live or terminal Run exists for that ID.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "503": { + "description": "Service credential is not initialized, the owner is fenced, or a run could not be reserved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "default": { + "description": "Other backend or reverse-proxy responses. Error formats vary; retain status, Content-Type and body.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/workflow/{id}/runs": { + "get": { + "operationId": "listWorkflowRuns", + "tags": [ + "Workflow integration" + ], + "summary": "List active and stored Workflow runs", + "description": "Combines active and stored summaries newest first; no pagination. TaskBoard records are projected only after source authorization. Optional Board/binding filters must be a valid authorized scope.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/workflow-integration" + }, + "security": [ + { + "WorkflowToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1 + } + }, + { + "name": "boardId", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 1 + }, + "description": "When requesting Task scope, boardId and workflowBindingId must be supplied together and belong to this Workflow. These IDs do not grant access." + }, + { + "name": "workflowBindingId", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 1 + }, + "description": "When requesting Task scope, boardId and workflowBindingId must be supplied together and belong to this Workflow. These IDs do not grant access." + }, + { + "name": "taskId", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 1 + }, + "description": "Optional nonempty Task narrowing; must match the Board/binding." + } + ], + "responses": { + "200": { + "description": "Visible active and stored summaries.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowRunList" + }, + "example": { + "runs": [ + { + "runId": "00000000-0000-4000-8000-000000000010", + "status": "completed", + "startedAt": "2026-10-14T02:00:00.000Z", + "nodeCount": 2, + "completedNodeCount": 2, + "taskBoardBound": false + } + ] + } + } + } + }, + "400": { + "description": "Workflow ID is missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "401": { + "description": "Missing or invalid standalone Workflow Service credential.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "403": { + "description": "Browser-origin or resource/TaskBoard scope authorization failed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "503": { + "description": "Service credential is not initialized, the owner is fenced, or a run could not be reserved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "default": { + "description": "Other backend or reverse-proxy responses. Error formats vary; retain status, Content-Type and body.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/workflow/{id}/runs/{runId}": { + "get": { + "operationId": "getWorkflowRun", + "tags": [ + "Workflow integration" + ], + "summary": "Read a Workflow Run detail", + "description": "Verifies Workflow/Run matching and current source resource access. An out-of-scope Run returns 404. A service token and Run ID do not replace TaskBoard authorization.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/workflow-integration" + }, + "security": [ + { + "WorkflowToken": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1 + } + }, + { + "name": "runId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 1 + } + }, + { + "name": "boardId", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 1 + }, + "description": "When requesting Task scope, boardId and workflowBindingId must be supplied together and belong to this Workflow. These IDs do not grant access." + }, + { + "name": "workflowBindingId", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 1 + }, + "description": "When requesting Task scope, boardId and workflowBindingId must be supplied together and belong to this Workflow. These IDs do not grant access." + }, + { + "name": "taskId", + "in": "query", + "required": false, + "schema": { + "type": "string", + "minLength": 1 + }, + "description": "Optional nonempty Task narrowing; must match the Board/binding." + } + ], + "responses": { + "200": { + "description": "Current active snapshot or stored terminal detail.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowRunDetail" + } + } + } + }, + "400": { + "description": "Workflow or Run ID is missing.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "401": { + "description": "Missing or invalid standalone Workflow Service credential.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "403": { + "description": "Browser-origin or resource/TaskBoard scope authorization failed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "404": { + "description": "The Run was not found or is outside the requested Task scope.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "503": { + "description": "Service credential is not initialized, the owner is fenced, or a run could not be reserved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "default": { + "description": "Other backend or reverse-proxy responses. Error formats vary; retain status, Content-Type and body.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/workflows": { + "get": { + "operationId": "listWorkflows", + "tags": [ + "Workflow integration" + ], + "summary": "List deployed Workflow metadata", + "description": "Lists readable parsed definitions with an optional compressed lastRun summary; invalid definitions are omitted. Discovery does not establish execution authorization. No pagination.", + "externalDocs": { + "url": "https://docs.desirecore.com/api/workflow-integration" + }, + "security": [ + { + "WorkflowToken": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Deployed Workflow metadata.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowList" + } + } + } + }, + "401": { + "description": "Missing or invalid standalone Workflow Service credential.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "403": { + "description": "Browser-origin or resource/TaskBoard scope authorization failed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "503": { + "description": "Service credential is not initialized, the owner is fenced, or a run could not be reserved.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowError" + } + } + } + }, + "default": { + "description": "Other backend or reverse-proxy responses. Error formats vary; retain status, Content-Type and body.", + "content": { + "application/json": { + "schema": {} + }, + "text/plain": { + "schema": { + "type": "string" + } + }, + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "x-source": { + "repository": "desirecore/desirecore", + "revision": "e45f22902af2dc45b109cfa8f454795538aea2ed" + } +} diff --git a/tsconfig.json b/tsconfig.json index 920d7a65..623e8211 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,5 +4,5 @@ "compilerOptions": { "baseUrl": "." }, - "exclude": [".docusaurus", "build"] + "exclude": [".docusaurus", "build", "api/examples/agent-client/test/source-route.fixture.ts"] }