From a4091d12eb33c6b72dd597c785dd62fab99d054f Mon Sep 17 00:00:00 2001 From: shwang Date: Fri, 9 Oct 2026 17:19:26 +0800 Subject: [PATCH 1/5] Prepare 0.2.0 candidate with reproducible diagrams and verified source packaging --- CHANGELOG.md | 16 ++++ README.md | 2 +- README.zh-CN.md | 2 +- RELEASING.md | 11 +++ assets/README.md | 6 ++ assets/execution-lifecycle-en.svg | 112 ++++++++++------------- assets/execution-lifecycle-zh.svg | 112 ++++++++++------------- docs/explanations/execution-lifecycle.md | 4 + package.json | 2 +- packages/conformance/package.json | 2 +- packages/core/package.json | 2 +- packages/core/src/runtime.ts | 2 +- packages/protocol/package.json | 2 +- packages/provider-acp/package.json | 2 +- packages/provider-acp/src/index.ts | 2 +- packages/provider-dsh/package.json | 2 +- packages/provider-dsh/src/index.ts | 2 +- packages/provider-mock/package.json | 2 +- packages/provider-mock/src/index.ts | 2 +- packages/sdk-typescript/package.json | 2 +- packages/server/package.json | 2 +- scripts/generate-lifecycle.mjs | 73 +++++++++++++++ scripts/generate-openapi.mjs | 3 + scripts/package-release.mjs | 69 ++++++++++++++ spec/openapi.yaml | 2 +- spec/runtime-model.md | 5 + tests/release-consistency.test.ts | 58 ++++++++++++ tests/release-package.test.ts | 80 ++++++++++++++++ 28 files changed, 437 insertions(+), 144 deletions(-) create mode 100644 scripts/generate-lifecycle.mjs create mode 100644 scripts/package-release.mjs create mode 100644 tests/release-consistency.test.ts create mode 100644 tests/release-package.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 312686d..63c37ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ changes. ## [Unreleased] +No unreleased changes. + +## [0.2.0] - 2026-10-09 + ### Added - ACP v1 subprocess Provider through the official SDK, with correlated human permissions, @@ -44,6 +48,18 @@ changes. - Added focused guides for one complete execution, provider ownership, and production deployment. - Reorganized the documentation map around evaluation, integration, provider development, and operations paths. +- Corrected both lifecycle diagrams to include failure while awaiting input or approval, with a + deterministic generator and release-version consistency tests. + +### Migration From 0.1.0 + +- DSH launch options must be set by the trusted Provider constructor; per-execution launch + overrides are rejected. Install exactly `@deepseek-ai/dsh-sdk-client@0.1.0-rc.7` and explicitly + declare only extensions verified in your deployment. +- Catch `HarnessRuntimeStreamInterruptedError` when an SSE stream ends before the execution is + terminal. Reconnect using the event cursor or query execution state before retrying work. +- The portable protocol remains `2026-09-02`. Reference storage remains in-memory; ACP is + experimental, and workspace packages are not published to npm. ## [0.1.0] - 2026-09-02 diff --git a/README.md b/README.md index 38ba9fd..d72d8bd 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ English | 简体中文

-> **Experimental:** runtime `0.1.0`, protocol `2026-09-02`. The reference implementation is +> **Experimental:** runtime `0.2.0`, protocol `2026-09-02`. The reference implementation is > in-memory, unauthenticated, and intended for development or one-process embedding. ![Harness Runtime API architecture: one application contract connects through capability-aware provider adapters to multiple peer harnesses.](assets/harness-runtime-architecture-en.png) diff --git a/README.zh-CN.md b/README.zh-CN.md index dfbde17..13d378f 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -8,7 +8,7 @@ English | 简体中文

-> **实验阶段:** Runtime `0.1.0`,协议 `2026-09-02`。当前参考实现使用内存状态、没有身份认证, +> **实验阶段:** Runtime `0.2.0`,协议 `2026-09-02`。当前参考实现使用内存状态、没有身份认证, > 适合本地开发、测试或单进程嵌入,不是可直接暴露到公网的多租户平台。 ![Harness Runtime API 架构:应用通过统一契约连接 Runtime,Provider SPI 基于能力预检适配多个并列 Harness,并统一回流事件。](assets/harness-runtime-architecture.png) diff --git a/RELEASING.md b/RELEASING.md index e403308..6f15feb 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -29,6 +29,17 @@ pnpm audit --prod --audit-level high ## GitHub Release +The first experimental release uses a source-only archive; it does not bundle dependencies, +credentials, local logs, or prebuilt binaries. From a clean reviewed commit, run: + +```bash +node scripts/package-release.mjs /tmp/harness-runtime-release +``` + +The output contains a Git archive, `release-manifest.json` bound to the exact commit, and +`SHA256SUMS`. Verify the archive in an isolated checkout using the frozen lockfile and documented +checks. Publish as a prerelease while the adapters and reference implementation are experimental. + After the exact tag and commit SHA are approved: 1. create an annotated `vX.Y.Z` tag at the reviewed commit; diff --git a/assets/README.md b/assets/README.md index 7d73111..ee8f16f 100644 --- a/assets/README.md +++ b/assets/README.md @@ -23,3 +23,9 @@ image and normative text disagree, schemas and specification documents win. | `capability-preflight-zh.svg` | Chinese | T0 deterministic | Exact capability decision flow | AnyCap-generated PNGs retain a neighboring `.prompt.md` production and validation record. + +The lifecycle SVGs are generated by `node scripts/generate-lifecycle.mjs`. Use `--check` to verify +reproducibility without writing files. On 2026-10-09, both languages were inspected in Chrome at +1600 x 900 and 1024 x 576: nine states, twelve transitions, readable labels, and no visible overlap. +Failure paths cover running and both human-wait states; the normative runtime model remains the +source of truth. Regression tests check these paths and generator synchronization. diff --git a/assets/execution-lifecycle-en.svg b/assets/execution-lifecycle-en.svg index 18c6ddd..75db9e9 100644 --- a/assets/execution-lifecycle-en.svg +++ b/assets/execution-lifecycle-en.svg @@ -1,66 +1,50 @@ - Portable execution lifecycle - Queued executions start and run, may wait for input or approval, and end as succeeded, failed, or cancelled. Cancellation can begin from any non-terminal state. - - - - - - - - Portable Execution Lifecycle - One bounded attempt, one terminal outcome - - - queued - - starting - - running - - - - - awaiting_input - - awaiting_approval - - - - - request - request - response - response - - - Cancel request - from any non-terminal state - - cancelling - - - - cancelled - - failed - - succeeded - - - - provider error - completed - - Terminal states never return to active states +Portable Execution Lifecycle +Provider failures can end running or awaiting states; cancellation applies to every non-terminal state. + + + + +Portable Execution Lifecycle +One bounded attempt, one terminal outcome +queued +starting +running +succeeded +awaiting_input +awaiting_approval +failed +cancelling +cancelled + +Cancel request +from any non-terminal state + + + + + + + + + + + + +completed +response +request +response +request +Provider failure +running or awaiting +Terminal states never return to active states. Cancellation owns its terminal outcome. diff --git a/assets/execution-lifecycle-zh.svg b/assets/execution-lifecycle-zh.svg index e267bb7..4e49c1e 100644 --- a/assets/execution-lifecycle-zh.svg +++ b/assets/execution-lifecycle-zh.svg @@ -1,66 +1,50 @@ - Portable Execution 生命周期 - 执行从排队、启动进入运行,可等待输入或审批,最终成功、失败或取消;任意非终态都可以发起取消。 - - - - - - - - Portable Execution 生命周期 - 一次有界执行,只产生一个终态结果 - - - queued 排队 - - starting 启动 - - running 运行 - - - - - awaiting_input - - awaiting_approval - - - - - 请求输入 - 请求审批 - 响应后恢复 - 响应后恢复 - - - 发起取消 - 可来自任意非终态 - - cancelling - - - - cancelled - - failed - - succeeded - - - - Provider 错误 - 执行完成 - - 终态不会返回活动状态 +执行生命周期 +运行及等待期间的 Provider 错误都可进入失败终态;所有非终态均可发起取消。 + + + + +执行生命周期 +一次有界执行,只产生一个终态结果 +queued排队 +starting启动 +running运行 +succeeded成功 +awaiting_input等待输入 +awaiting_approval等待审批 +failed失败 +cancelling正在取消 +cancelled已取消 + +发起取消 +适用于任意非终态 + + + + + + + + + + + + +执行完成 +响应恢复 +请求输入 +响应恢复 +请求审批 +Provider 错误 +运行或等待期间均可失败 +终态不会返回活动状态;取消流程拥有其最终取消结果。 diff --git a/docs/explanations/execution-lifecycle.md b/docs/explanations/execution-lifecycle.md index 10dc40b..05ca3f9 100644 --- a/docs/explanations/execution-lifecycle.md +++ b/docs/explanations/execution-lifecycle.md @@ -16,6 +16,10 @@ Exactly one of three terminal outcomes is retained: - `failed` after an unrecovered provider error; - `cancelled` after cancellation finalization. +Provider errors may occur in `running`, `awaiting_input`, or `awaiting_approval`. All three can +transition directly to `failed`; pending Actions are rejected rather than left orphaned. Returning +final output with an unresolved Action is also a failure, not a successful execution. + A cancellation request is accepted from any non-terminal state. Runtime moves through `cancelling`, aborts the provider context, rejects pending actions, invokes optional provider cleanup, and appends one `run.cancelled` terminal event. Terminal executions never return to active states. diff --git a/package.json b/package.json index 2a74718..9d6ab62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "harness-runtime-api", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "A provider-neutral runtime contract for AI agent harnesses.", "license": "Apache-2.0", diff --git a/packages/conformance/package.json b/packages/conformance/package.json index 6b7fa2c..6700875 100644 --- a/packages/conformance/package.json +++ b/packages/conformance/package.json @@ -1,6 +1,6 @@ { "name": "@harness-runtime/conformance", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "Provider conformance runner for Harness Runtime API.", "license": "Apache-2.0", diff --git a/packages/core/package.json b/packages/core/package.json index a1875b6..55a6156 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@harness-runtime/core", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "Provider SPI and reference runtime for Harness Runtime API.", "license": "Apache-2.0", diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index d178ae3..4fd1bb6 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -74,7 +74,7 @@ export class InMemoryHarnessRuntime { constructor(providers: HarnessProvider[] = [], options: InMemoryRuntimeOptions = {}) { this.name = options.name ?? 'harness-runtime-reference' - this.version = options.version ?? '0.1.0' + this.version = options.version ?? '0.2.0' for (const provider of providers) this.registerProvider(provider) } diff --git a/packages/protocol/package.json b/packages/protocol/package.json index b87675a..098eb59 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@harness-runtime/protocol", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "Schemas and types for the Harness Runtime API.", "license": "Apache-2.0", diff --git a/packages/provider-acp/package.json b/packages/provider-acp/package.json index 21b1e9f..93711a2 100644 --- a/packages/provider-acp/package.json +++ b/packages/provider-acp/package.json @@ -1,6 +1,6 @@ { "name": "@harness-runtime/provider-acp", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "ACP v1 subprocess adapter for Harness Runtime API.", "license": "Apache-2.0", diff --git a/packages/provider-acp/src/index.ts b/packages/provider-acp/src/index.ts index 8b4e132..e158279 100644 --- a/packages/provider-acp/src/index.ts +++ b/packages/provider-acp/src/index.ts @@ -49,7 +49,7 @@ export class AcpProvider implements HarnessProvider { this.manifest = ProviderManifestSchema.parse({ id: this.options.id, name: 'ACP v1 Agent Adapter', - version: '0.1.0', + version: '0.2.0', protocolVersion: PROTOCOL_VERSION, topologies: ['subprocess'], capabilities: { diff --git a/packages/provider-dsh/package.json b/packages/provider-dsh/package.json index 4218fa0..88918ce 100644 --- a/packages/provider-dsh/package.json +++ b/packages/provider-dsh/package.json @@ -1,6 +1,6 @@ { "name": "@harness-runtime/provider-dsh", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "Optional DeepSeek Harness adapter for Harness Runtime API.", "license": "Apache-2.0", diff --git a/packages/provider-dsh/src/index.ts b/packages/provider-dsh/src/index.ts index c9b1688..7c68858 100644 --- a/packages/provider-dsh/src/index.ts +++ b/packages/provider-dsh/src/index.ts @@ -77,7 +77,7 @@ interface ActiveDshClient { const manifest: ProviderManifest = { id: 'dsh', name: 'DeepSeek Harness Adapter', - version: '0.1.0', + version: '0.2.0', protocolVersion: PROTOCOL_VERSION, topologies: ['subprocess'], capabilities: { diff --git a/packages/provider-mock/package.json b/packages/provider-mock/package.json index d130eb0..51cf2b4 100644 --- a/packages/provider-mock/package.json +++ b/packages/provider-mock/package.json @@ -1,6 +1,6 @@ { "name": "@harness-runtime/provider-mock", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "Deterministic mock provider for Harness Runtime API.", "license": "Apache-2.0", diff --git a/packages/provider-mock/src/index.ts b/packages/provider-mock/src/index.ts index c2f7ab6..d445b72 100644 --- a/packages/provider-mock/src/index.ts +++ b/packages/provider-mock/src/index.ts @@ -8,7 +8,7 @@ import { const manifest: ProviderManifest = { id: 'mock', name: 'Deterministic Mock Provider', - version: '0.1.0', + version: '0.2.0', protocolVersion: PROTOCOL_VERSION, topologies: ['embedded'], capabilities: { diff --git a/packages/sdk-typescript/package.json b/packages/sdk-typescript/package.json index 6543244..8f61253 100644 --- a/packages/sdk-typescript/package.json +++ b/packages/sdk-typescript/package.json @@ -1,6 +1,6 @@ { "name": "@harness-runtime/sdk-typescript", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "TypeScript HTTP client for Harness Runtime API.", "license": "Apache-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index f628edf..5009015 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@harness-runtime/server", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "HTTP and SSE binding for Harness Runtime API.", "license": "Apache-2.0", diff --git a/scripts/generate-lifecycle.mjs b/scripts/generate-lifecycle.mjs new file mode 100644 index 0000000..4d69706 --- /dev/null +++ b/scripts/generate-lifecycle.mjs @@ -0,0 +1,73 @@ +import { readFile, writeFile } from 'node:fs/promises' + +const nodes = [ + ['queued', 60, 140, 180, 80, 'active', '排队'], + ['starting', 300, 140, 180, 80, 'active', '启动'], + ['running', 540, 140, 200, 80, 'active', '运行'], + ['succeeded', 1260, 140, 240, 80, '', '成功'], + ['awaiting_input', 350, 360, 250, 80, 'wait', '等待输入'], + ['awaiting_approval', 690, 360, 290, 80, 'wait', '等待审批'], + ['failed', 1260, 360, 240, 80, '', '失败'], + ['cancelling', 670, 650, 240, 90, 'wait', '正在取消'], + ['cancelled', 1260, 650, 240, 90, '', '已取消'], +] +const edges = [ + ['queued', 'starting', 'M240 180 H300'], + ['starting', 'running', 'M480 180 H540'], + ['running', 'succeeded', 'M740 180 H1260'], + ['running', 'awaiting_input', 'M600 220 V300 H475 V360'], + ['awaiting_input', 'running', 'M430 360 V260 H560 V220'], + ['running', 'awaiting_approval', 'M680 220 V315 H835 V360'], + ['awaiting_approval', 'running', 'M930 360 V275 H720 V220'], + ['running', 'failed', 'M740 205 H1110 V400 H1260'], + ['awaiting_input', 'failed', 'M600 400 H640 V505 H1110 V400 H1260'], + ['awaiting_approval', 'failed', 'M980 400 H1260'], + ['any_nonterminal', 'cancelling', 'M560 695 H670'], + ['cancelling', 'cancelled', 'M910 695 H1260'], +] +const text = (x, y, value, style = 'small') => + `${value}` + +for (const language of ['en', 'zh']) { + const zh = language === 'zh' + const title = zh ? '执行生命周期' : 'Portable Execution Lifecycle' + const svg = ` +${title} +${zh ? '运行及等待期间的 Provider 错误都可进入失败终态;所有非终态均可发起取消。' : 'Provider failures can end running or awaiting states; cancellation applies to every non-terminal state.'} + + + + +${text(800, 66, title, 'title')} +${text(800, 104, zh ? '一次有界执行,只产生一个终态结果' : 'One bounded attempt, one terminal outcome')} +${nodes.map(([name, x, y, width, height, style, label]) => `${text(x + width / 2, y + (zh ? 36 : 49), name, 'label')}${zh ? text(x + width / 2, y + 67, label) : ''}`).join('\n')} + +${text(310, 690, zh ? '发起取消' : 'Cancel request', 'label')} +${text(310, 722, zh ? '适用于任意非终态' : 'from any non-terminal state')} +${edges.map(([from, to, path]) => ``).join('\n')} +${text(990, 164, zh ? '执行完成' : 'completed')} +${text(430, 248, zh ? '响应恢复' : 'response')} +${text(515, 332, zh ? '请求输入' : 'request')} +${text(880, 261, zh ? '响应恢复' : 'response')} +${text(740, 342, zh ? '请求审批' : 'request')} +${text(1380, 478, zh ? 'Provider 错误' : 'Provider failure')} +${text(1380, 508, zh ? '运行或等待期间均可失败' : 'running or awaiting')} +${text(800, 832, zh ? '终态不会返回活动状态;取消流程拥有其最终取消结果。' : 'Terminal states never return to active states. Cancellation owns its terminal outcome.')} +\n` + const target = new URL(`../assets/execution-lifecycle-${language}.svg`, import.meta.url) + if (process.argv.includes('--check')) { + if ((await readFile(target, 'utf8')) !== svg) + throw new Error(`Stale lifecycle diagram: ${language}`) + } else { + await writeFile(target, svg) + } +} +console.log('Bilingual lifecycle SVGs verified with 12 explicit transitions') diff --git a/scripts/generate-openapi.mjs b/scripts/generate-openapi.mjs index 5746080..d7e9383 100644 --- a/scripts/generate-openapi.mjs +++ b/scripts/generate-openapi.mjs @@ -4,6 +4,9 @@ import { runtimeSchemas } from './openapi-schemas.mjs' const file = new URL('../spec/openapi.yaml', import.meta.url) const document = parse(await readFile(file, 'utf8')) +document.info.version = JSON.parse( + await readFile(new URL('../package.json', import.meta.url), 'utf8'), +).version document.components.schemas = runtimeSchemas() document.paths['/v1/conversations'].post.requestBody.content['application/json'].schema = { $ref: '#/components/schemas/CreateConversationRequest', diff --git a/scripts/package-release.mjs b/scripts/package-release.mjs new file mode 100644 index 0000000..19d6c8f --- /dev/null +++ b/scripts/package-release.mjs @@ -0,0 +1,69 @@ +import { execFileSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { mkdir, readFile, realpath, writeFile } from 'node:fs/promises' +import { basename, dirname, isAbsolute, relative, resolve, sep } from 'node:path' +import { fileURLToPath } from 'node:url' + +async function canonicalPath(path) { + const suffix = [] + for (;;) { + try { + return resolve(await realpath(path), ...suffix.reverse()) + } catch (error) { + if (error.code !== 'ENOENT') throw error + suffix.push(basename(path)) + path = dirname(path) + } + } +} + +const root = await realpath(fileURLToPath(new URL('..', import.meta.url))) +const git = (...args) => execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim() +const outputArgument = process.argv[2] +if (!outputArgument) throw new Error('Usage: node scripts/package-release.mjs ') +const output = await canonicalPath(resolve(outputArgument)) +const location = relative(root, output) +if (!isAbsolute(location) && location !== '..' && !location.startsWith(`..${sep}`)) { + throw new Error('Release output must be outside the repository') +} +if (git('status', '--porcelain')) + throw new Error('Release packaging requires a clean committed tree') +const commit = git('rev-parse', 'HEAD') +const manifest = JSON.parse(git('show', 'HEAD:package.json')) +if (!/^\d+\.\d+\.\d+$/.test(manifest.version)) + throw new Error('Expected a stable SemVer base version') +const name = `harness-runtime-api-${manifest.version}` +const archive = `${name}-source.tar.gz` +await mkdir(output, { recursive: true }) +execFileSync( + 'git', + ['archive', '--format=tar.gz', `--prefix=${name}/`, '-o', resolve(output, archive), commit], + { cwd: root }, +) +const sha256 = createHash('sha256') + .update(await readFile(resolve(output, archive))) + .digest('hex') +await writeFile( + resolve(output, 'release-manifest.json'), + `${JSON.stringify( + { + version: manifest.version, + commit, + format: 'source-only', + install: 'pnpm install --frozen-lockfile', + verification: 'pnpm check && pnpm sanitize', + packageRegistryPublished: false, + assets: [{ name: archive, sha256 }], + }, + null, + 2, + )}\n`, +) +const metadataHash = createHash('sha256') + .update(await readFile(resolve(output, 'release-manifest.json'))) + .digest('hex') +await writeFile( + resolve(output, 'SHA256SUMS'), + `${sha256} ${archive}\n${metadataHash} release-manifest.json\n`, +) +console.log(JSON.stringify({ version: manifest.version, commit, archive, sha256 })) diff --git a/spec/openapi.yaml b/spec/openapi.yaml index 20f0b65..d4a053c 100644 --- a/spec/openapi.yaml +++ b/spec/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: Harness Runtime API - version: 0.1.0 + version: 0.2.0 description: Provider-neutral control and event protocol for AI agent harness runtimes. servers: - url: http://127.0.0.1:4310 diff --git a/spec/runtime-model.md b/spec/runtime-model.md index aff3383..db3be8d 100644 --- a/spec/runtime-model.md +++ b/spec/runtime-model.md @@ -55,12 +55,17 @@ queued -> starting -> running |-> succeeded |-> failed any non-terminal state -> cancelling -> cancelled +running or awaiting_* -> failed on provider failure ``` Terminal states are `succeeded`, `failed`, and `cancelled`. A terminal execution never returns to a non-terminal state. Cancellation can begin from any non-terminal state; the provider-specific cancel hook and abort signal determine how native work is stopped. +Provider failures can also terminate an execution while it awaits input or approval. Returning +success with an unresolved Action is a provider failure; abandoned Action promises are settled. +Once cancellation begins, its cleanup path owns the terminal cancellation outcome. + ## Authority - The caller owns identity, authorization, business policy, and credentials. diff --git a/tests/release-consistency.test.ts b/tests/release-consistency.test.ts new file mode 100644 index 0000000..daa3944 --- /dev/null +++ b/tests/release-consistency.test.ts @@ -0,0 +1,58 @@ +import { execFileSync } from 'node:child_process' +import { readFile } from 'node:fs/promises' +import { describe, expect, it } from 'vitest' +import { parse } from 'yaml' +import { InMemoryHarnessRuntime } from '../packages/core/src/index.js' +import { MockProvider } from '../packages/provider-mock/src/index.js' +import { DshProvider } from '../packages/provider-dsh/src/index.js' +import { AcpProvider } from '../packages/provider-acp/src/index.js' + +const root = new URL('..', import.meta.url) +const read = (path: string) => readFile(new URL(path, root), 'utf8') + +describe('release metadata and exact diagrams', () => { + it('keeps workspace, runtime, provider, and OpenAPI versions aligned', async () => { + const { version } = JSON.parse(await read('package.json')) + for (const name of [ + 'protocol', + 'core', + 'provider-mock', + 'provider-dsh', + 'provider-acp', + 'conformance', + 'server', + 'sdk-typescript', + ]) { + expect(JSON.parse(await read(`packages/${name}/package.json`)).version).toBe(version) + } + expect(new InMemoryHarnessRuntime().describe().version).toBe(version) + expect(new MockProvider().manifest.version).toBe(version) + expect(new DshProvider({ launch: { command: 'synthetic-agent' } }).manifest.version).toBe( + version, + ) + expect(new AcpProvider({ command: 'synthetic-agent' }).manifest.version).toBe(version) + expect(parse(await read('spec/openapi.yaml')).info.version).toBe(version) + }) + + it('keeps both lifecycle diagrams synchronized with the deterministic generator', () => { + expect(() => + execFileSync( + process.execPath, + [new URL('scripts/generate-lifecycle.mjs', root).pathname, '--check'], + { stdio: 'pipe' }, + ), + ).not.toThrow() + }) + + it.each(['en', 'zh'])( + 'shows failure during both human waits in the %s diagram', + async (language) => { + const diagram = await read(`assets/execution-lifecycle-${language}.svg`) + expect(diagram.match(/data-state=/g)).toHaveLength(9) + expect(diagram.match(/data-from=/g)).toHaveLength(12) + for (const state of ['running', 'awaiting_input', 'awaiting_approval']) { + expect(diagram).toContain(`data-from="${state}" data-to="failed"`) + } + }, + ) +}) diff --git a/tests/release-package.test.ts b/tests/release-package.test.ts new file mode 100644 index 0000000..57935c8 --- /dev/null +++ b/tests/release-package.test.ts @@ -0,0 +1,80 @@ +import { execFileSync } from 'node:child_process' +import { createHash } from 'node:crypto' +import { copyFile, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' + +const directories: string[] = [] +afterEach(async () => { + await Promise.all( + directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), + ) +}) + +async function fixture() { + const directory = await mkdtemp(join(tmpdir(), 'harness-release-test-')) + directories.push(directory) + const repo = join(directory, 'repo') + const output = join(directory, 'output') + await mkdir(join(repo, 'scripts'), { recursive: true }) + await copyFile( + new URL('../scripts/package-release.mjs', import.meta.url), + join(repo, 'scripts/package-release.mjs'), + ) + await writeFile(join(repo, 'package.json'), JSON.stringify({ version: '0.2.0', private: true })) + const git = (...args: string[]) => + execFileSync('git', args, { cwd: repo, encoding: 'utf8', stdio: 'pipe' }).trim() + git('init', '--quiet') + git('add', '.') + git( + '-c', + 'user.name=Synthetic Test', + '-c', + 'user.email=test@example.invalid', + '-c', + 'commit.gpgsign=false', + 'commit', + '--quiet', + '-m', + 'Synthetic fixture', + ) + const run = (destination = output) => + execFileSync(process.execPath, [join(repo, 'scripts/package-release.mjs'), destination], { + cwd: repo, + encoding: 'utf8', + stdio: 'pipe', + }) + return { repo, output, git, run } +} + +describe('source-only release packaging', () => { + it('binds archive and checksums to the exact clean commit without Git metadata', async () => { + const { output, git, run } = await fixture() + const result = JSON.parse(run()) + expect(result.commit).toBe(git('rev-parse', 'HEAD')) + const archive = await readFile(join(output, result.archive)) + expect(result.sha256).toBe(createHash('sha256').update(archive).digest('hex')) + const metadata = await readFile(join(output, 'release-manifest.json')) + expect(JSON.parse(metadata.toString()).packageRegistryPublished).toBe(false) + expect(await readFile(join(output, 'SHA256SUMS'), 'utf8')).toContain( + `${createHash('sha256').update(metadata).digest('hex')} release-manifest.json`, + ) + const files = execFileSync('tar', ['-tzf', join(output, result.archive)], { encoding: 'utf8' }) + .trim() + .split('\n') + expect(files.every((file) => file.startsWith('harness-runtime-api-0.2.0/'))).toBe(true) + expect(files.some((file) => file.includes('/.git/'))).toBe(false) + }) + + it('rejects uncommitted source', async () => { + const { repo, run } = await fixture() + await writeFile(join(repo, 'unreviewed.txt'), 'synthetic') + expect(run).toThrow() + }) + + it('rejects output inside the source repository', async () => { + const { repo, run } = await fixture() + expect(() => run(join(repo, 'release'))).toThrow() + }) +}) From c97c45d4e25f8de49a95d5b0f42655f979deedcc Mon Sep 17 00:00:00 2001 From: shwang Date: Fri, 9 Oct 2026 17:26:42 +0800 Subject: [PATCH 2/5] Stabilize subprocess tests under load and write synthetic traces atomically --- CHANGELOG.md | 2 ++ tests/fixtures/acp-agent.mjs | 9 ++++++--- tests/integration/acp-provider.integration.ts | 8 ++++---- tests/integration/dsh-sdk.integration.ts | 6 +++--- tests/integration/http-server.integration.ts | 5 ++++- vitest.integration.config.ts | 4 +++- 6 files changed, 22 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63c37ba..152dc3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ No unreleased changes. cleanup errors and timeouts included in the report. - Updated Hono to 4.13.7 to clear four production dependency advisories, including [GHSA-hxh3-vqpv-xpqv](https://github.com/honojs/hono/security/advisories/GHSA-hxh3-vqpv-xpqv). +- Subprocess integration tests run serially with bounded startup allowances instead of assuming + idle-machine latency. Synthetic ACP traces use atomic replacement to avoid partial JSON reads. ### Documentation diff --git a/tests/fixtures/acp-agent.mjs b/tests/fixtures/acp-agent.mjs index 5668d66..72d01bc 100644 --- a/tests/fixtures/acp-agent.mjs +++ b/tests/fixtures/acp-agent.mjs @@ -1,4 +1,4 @@ -import { writeFileSync } from 'node:fs' +import { renameSync, writeFileSync } from 'node:fs' import { isAbsolute } from 'node:path' import { Readable, Writable } from 'node:stream' import { createInterface } from 'node:readline' @@ -18,8 +18,11 @@ const state = { parseErrors: 0, } const record = () => { - if (process.env.FIXTURE_TRACE_PATH) - writeFileSync(process.env.FIXTURE_TRACE_PATH, JSON.stringify(state)) + const path = process.env.FIXTURE_TRACE_PATH + if (path) { + writeFileSync(`${path}.tmp`, JSON.stringify(state)) + renameSync(`${path}.tmp`, path) + } } record() const inputObserver = createInterface({ input: process.stdin }) diff --git a/tests/integration/acp-provider.integration.ts b/tests/integration/acp-provider.integration.ts index 5c8cce6..3fb4a1d 100644 --- a/tests/integration/acp-provider.integration.ts +++ b/tests/integration/acp-provider.integration.ts @@ -32,8 +32,8 @@ async function setup(mode: string, maxMessageBytes?: number, maxPendingPermissio command: process.execPath, args: [fixture], env: { FIXTURE_MODE: mode, FIXTURE_TRACE_PATH: tracePath, FIXTURE_DENIED_TARGET: deniedTarget }, - initializationTimeoutMs: 1_000, - runTimeoutMs: 3_000, + initializationTimeoutMs: 5_000, + runTimeoutMs: 10_000, shutdownGraceMs: 100, ...(maxMessageBytes === undefined ? {} : { maxMessageBytes }), ...(maxPendingPermissions === undefined ? {} : { maxPendingPermissions }), @@ -271,9 +271,9 @@ describe('ACP v1 adapter through the official SDK and owned subprocess', () => { const report = await runProviderConformance(task.provider, { requiredCapabilities: ['action.approval'], actionResponse: { approved: true }, - timeoutMs: 5_000, + timeoutMs: 15_000, }) - expect(report.passed).toBe(true) + expect(report.passed, JSON.stringify(report)).toBe(true) } finally { await task.cleanup() } diff --git a/tests/integration/dsh-sdk.integration.ts b/tests/integration/dsh-sdk.integration.ts index e123a55..7b1bec1 100644 --- a/tests/integration/dsh-sdk.integration.ts +++ b/tests/integration/dsh-sdk.integration.ts @@ -16,7 +16,7 @@ function providerFor(mode: string) { command: process.execPath, args: [fixture], env: { FIXTURE_MODE: mode }, - requestTimeoutMs: 1_000, + requestTimeoutMs: 5_000, shutdownTimeoutMs: 100, disposeEofGraceMs: 200, disposeGraceMs: 200, @@ -107,7 +107,7 @@ describe('Pinned public DSH SDK with a real synthetic subprocess', () => { }) it('passes the reusable conformance probe through the actual SDK', async () => { - const report = await runProviderConformance(providerFor('success'), { timeoutMs: 5_000 }) - expect(report.passed).toBe(true) + const report = await runProviderConformance(providerFor('success'), { timeoutMs: 10_000 }) + expect(report.passed, JSON.stringify(report)).toBe(true) }) }) diff --git a/tests/integration/http-server.integration.ts b/tests/integration/http-server.integration.ts index 1376a7c..8962041 100644 --- a/tests/integration/http-server.integration.ts +++ b/tests/integration/http-server.integration.ts @@ -40,7 +40,10 @@ beforeAll(async () => { const lines = createInterface({ input: server.stdout }) try { baseUrl = await new Promise((resolve, reject) => { - const timer = setTimeout(() => reject(new Error('Reference server startup timed out')), 3_000) + const timer = setTimeout( + () => reject(new Error('Reference server startup timed out')), + 10_000, + ) const cleanup = () => { clearTimeout(timer) server.removeListener('exit', failed) diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts index 2666f76..f61125f 100644 --- a/vitest.integration.config.ts +++ b/vitest.integration.config.ts @@ -3,6 +3,8 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { include: ['tests/integration/**/*.integration.ts'], - testTimeout: 15_000, + fileParallelism: false, + testTimeout: 30_000, + hookTimeout: 15_000, }, }) From e57a1795d35f760897c18341f4c4df322d8c34b4 Mon Sep 17 00:00:00 2001 From: shwang Date: Fri, 9 Oct 2026 17:29:51 +0800 Subject: [PATCH 3/5] Reconcile candidate evidence and exact DSH compatibility documentation --- docs/development/delivery-plan.md | 31 +++++++++++++++++++++++++------ packages/provider-dsh/README.md | 2 +- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/docs/development/delivery-plan.md b/docs/development/delivery-plan.md index fca4035..68acfe2 100644 --- a/docs/development/delivery-plan.md +++ b/docs/development/delivery-plan.md @@ -17,7 +17,7 @@ delivery does not claim those features exist in the reference server. malformed inputs, rather than only in-process request helpers. - [x] Runnable examples cover application integration and provider implementation; a fresh checkout can follow documented commands without private services or model credentials. -- [ ] Protocol, OpenAPI, bilingual README, provider guides, and release materials agree with code. +- [x] Protocol, OpenAPI, bilingual README, provider guides, and release materials agree with code. - [ ] The final tree, public history, dependency inventory, and publishable artifacts pass privacy and security review; findings and exclusions are stated explicitly. - [ ] Local checks and the reviewed GitHub commit's CI pass; the worktree is clean and synchronized. @@ -46,14 +46,33 @@ delivery does not claim those features exist in the reference server. ## Remaining Work -1. Finish documentation/visual consistency review for the complete delivery, including failure - while awaiting a human Action. -2. Finish source/history/media/artifact privacy review and release packaging/version decisions. -3. Run final local and GitHub checks for the complete delivery, verify fresh-checkout instructions, - then prepare the exact candidate required by the release authorization policy. +1. Complete final local and isolated source-archive checks after the subprocess test stability fix. +2. Reconcile final history/artifact audit evidence with the reviewed delivery commit. +3. Record the exact release tag and commit for separate publication approval. No completion claim has been made for the unchecked acceptance items. +## 0.2.0 Candidate + +- [PR 12](https://github.com/capa-cloud/harness-runtime-api/pull/12) prepares runtime/workspace + version `0.2.0`, retaining portable protocol `2026-09-02` and adding migration notes. +- Bilingual lifecycle diagrams now show failure during both human waits. Chrome inspections at + original and reading size passed; automated checks cover nine states, twelve transitions, and + deterministic regeneration. +- Source packaging requires a clean commit and an output directory outside the repository, + including through symbolic links. Archives exclude Git metadata and include commit-bound + metadata and SHA-256 checksums. Packages remain unpublished. +- Seven added version/diagram/packaging regression tests passed. Candidate + `c97c45d` passed [Node 22/24 CI](https://github.com/capa-cloud/harness-runtime-api/actions/runs/37911321061) + with 62 unit/contract tests and 35 integration tests per environment. +- A complete local reachable-history scan found no credentials or private fingerprints; the + current-tree audit covered 137 files and metadata of eleven PNGs. Final artifacts are rechecked + after the reviewed commit is frozen; scans do not prove an absence of every possible secret. +- Initial local source/archive reruns exposed overly tight subprocess deadlines and a partial + synthetic trace write under load. The tests now run serially with bounded startup allowances; + trace writes use atomic replacement. The assertions and production provider defaults are unchanged. + Full local and isolated-archive reruns remain required before final delivery is claimed. + ## ACP and Schema Verification - ACP v1 implementation and boundary hardening were developed in diff --git a/packages/provider-dsh/README.md b/packages/provider-dsh/README.md index 5b7929e..bd55055 100644 --- a/packages/provider-dsh/README.md +++ b/packages/provider-dsh/README.md @@ -1,6 +1,6 @@ # DeepSeek Harness Provider -Optional subprocess adapter for `@deepseek-ai/dsh-sdk-client` `0.1.0-rc.7` or a compatible release. +Optional subprocess adapter for exactly `@deepseek-ai/dsh-sdk-client` `0.1.0-rc.7`. The dependency is loaded only when the provider starts an execution. Unit tests use an injected structural client and never invoke a model or require a credential. From c8aa77efb98860a76e80919e999b369a4d760f08 Mon Sep 17 00:00:00 2001 From: shwang Date: Fri, 9 Oct 2026 17:37:52 +0800 Subject: [PATCH 4/5] Allow bounded ACP startup and exit scheduling latency in integration fixtures --- tests/integration/acp-provider.integration.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration/acp-provider.integration.ts b/tests/integration/acp-provider.integration.ts index 3fb4a1d..ce9c8cb 100644 --- a/tests/integration/acp-provider.integration.ts +++ b/tests/integration/acp-provider.integration.ts @@ -32,9 +32,9 @@ async function setup(mode: string, maxMessageBytes?: number, maxPendingPermissio command: process.execPath, args: [fixture], env: { FIXTURE_MODE: mode, FIXTURE_TRACE_PATH: tracePath, FIXTURE_DENIED_TARGET: deniedTarget }, - initializationTimeoutMs: 5_000, + initializationTimeoutMs: 10_000, runTimeoutMs: 10_000, - shutdownGraceMs: 100, + shutdownGraceMs: 1_000, ...(maxMessageBytes === undefined ? {} : { maxMessageBytes }), ...(maxPendingPermissions === undefined ? {} : { maxPendingPermissions }), }) @@ -242,6 +242,7 @@ describe('ACP v1 adapter through the official SDK and owned subprocess', () => { const task = await setup('bad-json') try { await drain(task.runtime, task.execution.id) + expect(task.runtime.getExecution(task.execution.id).error).toBeUndefined() expect(task.runtime.getExecution(task.execution.id).state).toBe('succeeded') expect((await task.trace()).parseErrors).toBe(1) expect(diagnostics).not.toHaveBeenCalled() @@ -256,6 +257,7 @@ describe('ACP v1 adapter through the official SDK and owned subprocess', () => { const task = await setup('forbidden-client') try { await drain(task.runtime, task.execution.id) + expect(task.runtime.getExecution(task.execution.id).error).toBeUndefined() expect(task.runtime.getExecution(task.execution.id).state).toBe('succeeded') expect((await task.trace()).forbiddenErrors).toEqual([-32601, -32601, -32601]) expect(await task.protectedText()).toBe('synthetic protected text') From 55df5bbf15136e53c9a8f6009d7d5dbc9f7c0d59 Mon Sep 17 00:00:00 2001 From: shwang Date: Fri, 9 Oct 2026 17:43:27 +0800 Subject: [PATCH 5/5] Record passing final source archive and security verification --- docs/development/delivery-plan.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/development/delivery-plan.md b/docs/development/delivery-plan.md index 68acfe2..0dee88b 100644 --- a/docs/development/delivery-plan.md +++ b/docs/development/delivery-plan.md @@ -18,9 +18,9 @@ delivery does not claim those features exist in the reference server. - [x] Runnable examples cover application integration and provider implementation; a fresh checkout can follow documented commands without private services or model credentials. - [x] Protocol, OpenAPI, bilingual README, provider guides, and release materials agree with code. -- [ ] The final tree, public history, dependency inventory, and publishable artifacts pass privacy +- [x] The final tree, public history, dependency inventory, and publishable artifacts pass privacy and security review; findings and exclusions are stated explicitly. -- [ ] Local checks and the reviewed GitHub commit's CI pass; the worktree is clean and synchronized. +- [x] Local checks and the reviewed GitHub commit's CI pass; the worktree is clean and synchronized. - [ ] The exact release candidate, version, packaging decision, and any required publication authorization are recorded before claiming final delivery. @@ -46,9 +46,8 @@ delivery does not claim those features exist in the reference server. ## Remaining Work -1. Complete final local and isolated source-archive checks after the subprocess test stability fix. -2. Reconcile final history/artifact audit evidence with the reviewed delivery commit. -3. Record the exact release tag and commit for separate publication approval. +1. Record the final merged commit for `v0.2.0` and obtain separate immutable-tag/GitHub prerelease + publication approval. Source-only packaging is prepared; npm publication is not part of delivery. No completion claim has been made for the unchecked acceptance items. @@ -71,7 +70,16 @@ No completion claim has been made for the unchecked acceptance items. - Initial local source/archive reruns exposed overly tight subprocess deadlines and a partial synthetic trace write under load. The tests now run serially with bounded startup allowances; trace writes use atomic replacement. The assertions and production provider defaults are unchanged. - Full local and isolated-archive reruns remain required before final delivery is claimed. + The final code revision `c8aa77e` passed both full local and isolated-archive reruns: each had + 62 unit/contract tests and 35 integration tests, format/lint/type/build checks, and public scanning. + The exact revision also passed [Node 22/24 CI](https://github.com/capa-cloud/harness-runtime-api/actions/runs/37912523565). + ACP fixture initialization and cleanup allowances are 10 seconds and one second; failure + diagnostics are explicit, and production defaults remain unchanged. +- The final code audit covered 137 files, eleven PNGs, and 29 reachable commits, with zero private + fingerprint or credential findings. Source archive and manifest SHA-256 verification passed. + Source bundles include the frozen dependency inventory; production dependency auditing reported + no known vulnerabilities. Native model quality, operating-system isolation, and production load + testing remain outside this credential-free delivery validation. ## ACP and Schema Verification