Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ changes.

## [Unreleased]

### Added

- ACP v1 subprocess Provider through the official SDK, with correlated human permissions,
one-time versus explicit remembered choices, serialized concurrent requests, and bounded process
teardown. Client filesystem/terminal access is disabled; native isolation remains deployment-owned.
- Generated OpenAPI schema projection and independent contract/HTTP compatibility checks.
- Real ACP transport tests using a synthetic Agent, including scope violations, malformed
messages, diagnostic redaction, process exit, and permission cancellation.

### Fixed

- Provider emissions now reject reserved lifecycle events and malformed portable payloads before
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Harness Runtime API introduces three explicit boundaries:
| Artifact descriptors | Available | Validated creation events and list API; bytes stay outside Runtime |
| Mock Provider | Available | Deterministic development and conformance provider |
| DSH Provider | Experimental | SDK `0.1.0-rc.7`; extensions require an explicit deployment declaration |
| ACP v1 Provider | Experimental | SDK `1.7.0`; permissions, text updates, and bounded subprocess cancellation |
| Durable persistence and recovery | Not included | Current reference state is process-local |
| Authentication and multi-tenancy | Not included | Must be supplied by a trusted deployment boundary |

Expand Down Expand Up @@ -165,6 +166,7 @@ idempotency, capability preflight, cursor replay, terminal-state semantics, and
| `@harness-runtime/core` | Provider SPI and in-memory reference runtime |
| `@harness-runtime/provider-mock` | Deterministic provider for development and tests |
| `@harness-runtime/provider-dsh` | Optional DeepSeek Harness subprocess adapter |
| `@harness-runtime/provider-acp` | ACP v1 subprocess adapter with explicit human permission mapping |
| `@harness-runtime/server` | HTTP/JSON and SSE reference binding |
| `@harness-runtime/sdk-typescript` | Validating TypeScript HTTP/SSE client |
| `@harness-runtime/conformance` | Reusable provider lifecycle checks |
Expand All @@ -184,6 +186,7 @@ Package names are workspace identifiers during the MVP and are not yet published
- [Portable and provider-native boundary](docs/explanations/provider-boundary.md)
- [Artifact descriptors](docs/guides/artifacts.md)
- [DeepSeek Harness adapter](docs/guides/dsh-provider.md)
- [ACP v1 adapter](docs/guides/acp-provider.md)
- [Production deployment boundary](docs/guides/production-deployment.md)
- [Architecture decisions](spec/README.md)
- [Security policy](SECURITY.md)
Expand Down
3 changes: 3 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Harness Runtime API 建立三个明确边界:
| Artifact 描述符 | 已提供 | 创建事件与列表 API;文件内容不进入 Runtime |
| Mock Provider | 已提供 | 确定性的开发和一致性测试 Provider |
| DSH Provider | 实验性 | SDK `0.1.0-rc.7`;原生扩展需由可信部署显式声明 |
| ACP v1 Provider | 实验性 | SDK `1.7.0`;权限请求、文本更新和有界子进程取消 |
| 持久化与重启恢复 | 未提供 | 当前参考状态只存在于进程内 |
| 身份认证与多租户 | 未提供 | 必须由可信部署边界提供 |

Expand Down Expand Up @@ -156,6 +157,7 @@ Adapter 内部可以使用 ACP 或框架 SDK。Portable Runtime 额外定义幂
| `@harness-runtime/core` | Provider SPI 与内存参考 Runtime |
| `@harness-runtime/provider-mock` | 用于开发和测试的确定性 Provider |
| `@harness-runtime/provider-dsh` | 可选的 DeepSeek Harness 子进程适配器 |
| `@harness-runtime/provider-acp` | ACP v1 子进程适配器,显式映射人工权限选择 |
| `@harness-runtime/server` | HTTP/JSON 与 SSE 参考绑定 |
| `@harness-runtime/sdk-typescript` | 带响应校验的 TypeScript HTTP/SSE Client |
| `@harness-runtime/conformance` | 可复用的 Provider 生命周期一致性检查 |
Expand All @@ -175,6 +177,7 @@ MVP 阶段这些名称只作为 workspace 标识,尚未发布到包注册中
- [Portable 与 Provider Native 边界](docs/explanations/provider-boundary.md)
- [Artifact 描述符](docs/guides/artifacts.md)
- [DeepSeek Harness Adapter](docs/guides/dsh-provider.md)
- [ACP v1 Adapter](docs/guides/acp-provider.md)
- [生产部署边界](docs/guides/production-deployment.md)
- [架构决策](spec/README.md)
- [安全策略](SECURITY.md)
Expand Down
6 changes: 6 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ The reference implementation does not write execution transcripts to disk or log

`pnpm sanitize` uses ripgrep when available and a grep fallback otherwise. Scanner/tool errors fail
the check. Matches list file names only; suspected credential values are never printed by the scanner.

The ACP adapter uses a copied allowlist environment and direct argv launch. Client-side filesystem
and terminal capabilities are disabled; this does not constrain an agent process's own OS access.
Permission grants require explicit approval and remembered grants require an explicit option ID.
Inbound protocol envelopes are validated before SDK routing to avoid raw diagnostic logging.
Apply a deployment-owned sandbox and policies for native events before using real agents.
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Current protocol version: `2026-09-02`
| Review state and invariants | [Runtime model](../spec/runtime-model.md) |
| Implement Artifact handling | [Artifact descriptors](guides/artifacts.md) |
| Configure the DSH adapter | [DeepSeek Harness Provider](guides/dsh-provider.md) |
| Configure an ACP executable | [ACP v1 Provider](guides/acp-provider.md) |
| Compare neighboring protocols | [Protocol stack](explanations/protocol-stack.md) |
| Understand execution transitions | [Execution lifecycle](explanations/execution-lifecycle.md) |
| Understand one complete run | [Execution control loop](explanations/execution-control-loop.md) |
Expand Down
29 changes: 23 additions & 6 deletions docs/development/delivery-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ delivery does not claim those features exist in the reference server.

- [x] DSH compatibility is pinned to an actually tested public SDK, with subprocess integration
tests, cancellation, startup failure, and explicit environment isolation.
- [ ] A second public Harness integration proves the provider boundary beyond Mock and DSH.
- [x] A second public Harness integration proves the provider boundary beyond Mock and DSH.
- [x] Capability-specific conformance probes cover advertised actions, cancellation, replay,
idempotency, and artifacts without assuming model behavior portability.
- [x] HTTP and SDK verification uses real loopback connections, including disconnection and
Expand Down Expand Up @@ -46,15 +46,32 @@ delivery does not claim those features exist in the reference server.

## Remaining Work

1. Implement ACP v1 integration through the public SDK, including session updates, explicit human
permissions, bounded cancellation, and denial of unadvertised client filesystem/terminal access.
2. Add executable OpenAPI/schema constraint parity and stronger negative HTTP verification.
3. Finish source/history/media/artifact privacy review and release packaging/version decisions.
4. Run final local and GitHub checks for the complete delivery, verify fresh-checkout instructions,
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.

No completion claim has been made for the unchecked acceptance items.

## ACP and Schema Verification

- ACP v1 implementation and boundary hardening were developed in
[PR 11](https://github.com/capa-cloud/harness-runtime-api/pull/11).
- [Reviewed provider/schema CI](https://github.com/capa-cloud/harness-runtime-api/actions/runs/37906236553)
passed on Node 22 and 24. The final bounded-permission revision passed local full checks with
55 unit/contract tests and 35 integration tests.
- ACP tests use the official SDK on both sides of a real synthetic subprocess. They verify human
permission choices, simultaneous requests, cancellation while awaiting approval, forced process
exit, isolated environments, denied client file/terminal requests, unrelated-session updates,
malformed-message handling, and no raw diagnostic logging.
- Pending native permissions are bounded (default: 16); the overflow regression confirms an agent
cannot create an unbounded queue while awaiting a human response.
- OpenAPI now projects 23 named Zod schemas with generated references and constraints. Independent
JSON Schema samples and actual HTTP payload checks passed; native timestamp patterns carry the
input constraints and format is treated as an annotation.
- No real model, private platform, production resource, or user session history was accessed.

## Verification Boundaries

Default tests use synthetic data and local processes only. Native Harness/model integration is
Expand Down
89 changes: 89 additions & 0 deletions docs/guides/acp-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# ACP v1 Provider

As of 2026-10-09. SDK version: `1.7.0`, stable ACP v1 entry point.

The adapter connects a trusted ACP executable through the official TypeScript SDK. It does not
use the experimental v2 protocol, implement an agent loop, or require a model credential by itself.

```ts
import { AcpProvider } from '@harness-runtime/provider-acp'

const provider = new AcpProvider({
command: 'path/to/your-acp-agent',
args: ['--acp'],
cwd: 'path/to/isolated-workspace',
env: allowlistedAgentEnvironment(),
initializationTimeoutMs: 10_000,
runTimeoutMs: 300_000,
shutdownGraceMs: 500,
maxMessageBytes: 1_048_576,
maxPendingPermissions: 16,
})
```

`allowlistedAgentEnvironment()` is application-owned; the default environment is empty. Prefer an
absolute executable path. Arguments are passed directly without a shell. Constructor arguments and
environment are copied, while per-execution `config` must remain empty. Register multiple native
agents with distinct trusted `id` values rather than letting requests select arbitrary executables.

## Mapping

| ACP surface | Portable behavior |
| --- | --- |
| Initialization and new session | One fresh native session per execution |
| Assistant text chunks | `output.text.delta`; concatenated assistant text becomes final output |
| Other session updates | Namespaced `provider.event` observations |
| Permission request | `action.required`, awaiting approval, then a correlated response |
| Prompt result | Native stop reason retained under `provider.event` |
| Native cancellation | Hint plus bounded EOF/SIGTERM/SIGKILL teardown; advertised as emulated |
| Client filesystem and terminal | Disabled capabilities and no registered handlers |
| Artifacts, user input, continuity, native extensions | Unsupported by this adapter |

The [official prompt-turn contract](https://agentclientprotocol.com/protocol/v1/prompt-turn) separates
session updates from prompt completion. A portable success means the native turn and teardown
completed normally, not that the agent fulfilled the business task. Non-text content stays in
native events; the adapter never fetches files or converts tool output into Artifact descriptors.

## Human Permissions

The [official permission contract](https://agentclientprotocol.com/protocol/v1/tool-calls) distinguishes
one-time decisions from remembered decisions. This adapter never silently expands an approval:

- `{ approved: true }` selects an available `allow_once` option, otherwise cancels.
- `{ approved: false }` selects an available `reject_once` option, otherwise cancels.
- An explicit option may be supplied as `value: { optionId: "..." }` or a string ID. Grant options
also require `approved: true`; contradictory, unknown, malformed, or ambiguous choices cancel.
- `allow_always` is honored only when that specific option was explicitly selected and approved.
- Cancellation answers pending requests with a cancelled outcome before process teardown.

The application supplies business approval policy. The adapter serializes concurrent permissions
but does not automatically approve them. Remembered decisions are owned by the native agent, not a
portable permission store.

## Security Boundary

The adapter rejects unrelated-session updates, bounds inbound message size and run duration,
discards stderr, and returns generic request errors instead of forwarding native diagnostics.
Incoming envelopes and registered notification parameters are checked against the SDK's public
JSON Schema before routing, so malformed messages cannot reach its raw diagnostic logger. JSON
syntax errors retain the SDK's parse-error reply/recovery behavior and are not logged with raw input.
Native updates may still contain sensitive tool or file information; restrict access and redact
before exporting those events. These protections do not isolate the native process's filesystem,
network, subprocesses, or descendants. Use a deployment-owned sandbox and least-privilege account.

The process teardown confirms the direct owned child's exit and transport closure. It does not
certify that every descendant, remote request, or native side effect has stopped.

## Verification

```bash
pnpm test:integration
```

Credential-free tests use the actual public SDK on both sides of a real subprocess connection.
They cover text normalization, environment isolation, permission choices and cancellation,
concurrent permission requests, forced exit, malformed peers, unrelated sessions, message limits,
and denied client file/terminal access. No private platform or model endpoint is contacted.

This proves adapter/protocol behavior against a synthetic Agent. Verify each real executable's
authentication, tool policy, native isolation, and model behavior separately before deployment.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@
"format:check": "biome format .",
"lint": "biome lint .",
"sanitize": "bash scripts/scan-public.sh .",
"spec:generate": "pnpm build && node scripts/generate-openapi.mjs",
"test": "pnpm build && vitest run",
"test:integration": "pnpm build && vitest run --config vitest.integration.config.ts",
"typecheck": "tsc -b && tsc -p tsconfig.tests.json --noEmit"
},
"devDependencies": {
"ajv": "8.20.0",
"zod": "4.4.3",
"@agentclientprotocol/sdk": "1.7.0",
"@deepseek-ai/dsh-sdk-client": "0.1.0-rc.7",
"@biomejs/biome": "2.5.9",
"@types/node": "22.20.1",
Expand Down
18 changes: 18 additions & 0 deletions packages/provider-acp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# ACP v1 Provider

Subprocess adapter built on the public `@agentclientprotocol/sdk` `1.7.0` stable v1 entry point.
It maps assistant text updates and native permission requests into the portable Runtime contract.

The trusted constructor owns executable, arguments, working directory, environment, timeouts, and
message limits. Execution requests cannot override those values. Each execution owns one process
and a fresh native session; conversation continuity and checkpoint portability are not advertised.

Permission requests become correlated approval Actions. Approve-once selects `allow_once` only;
`allow_always` requires an explicit option ID and `approved: true`. Invalid or contradictory choices
are cancelled. Concurrent requests are serialized into the Runtime's single pending Action slot.

Client filesystem and terminal capabilities are disabled and no handlers implement them. Native
agent processes retain their own operating-system privileges, so this is not sandbox isolation.
Cancellation uses a native notification followed by bounded owned-process teardown.

See the [integration guide](../../docs/guides/acp-provider.md) for usage and verification boundaries.
29 changes: 29 additions & 0 deletions packages/provider-acp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@harness-runtime/provider-acp",
"version": "0.1.0",
"private": true,
"description": "ACP v1 subprocess adapter for Harness Runtime API.",
"license": "Apache-2.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"ajv": "8.20.0",
"@agentclientprotocol/sdk": "1.7.0",
"@harness-runtime/core": "workspace:*",
"@harness-runtime/protocol": "workspace:*"
}
}
90 changes: 90 additions & 0 deletions packages/provider-acp/src/boundary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import type { AnyMessage, Stream } from '@agentclientprotocol/sdk'
import schema from '@agentclientprotocol/sdk/schema/schema.json' with { type: 'json' }
import { Ajv2020 } from 'ajv/dist/2020.js'

const validator = new Ajv2020({ strict: false, strictNumbers: true, validateFormats: false })
validator.addSchema({ ...schema, $id: 'https://runtime.test/acp-schema' })
const messageValid = validator.compile({ $ref: 'https://runtime.test/acp-schema' })
const parameterValidators = {
'session/update': validator.compile({
$ref: 'https://runtime.test/acp-schema#/$defs/SessionNotification',
}),
'session/request_permission': validator.compile({
$ref: 'https://runtime.test/acp-schema#/$defs/RequestPermissionRequest',
}),
'$/cancel_request': validator.compile({
$ref: 'https://runtime.test/acp-schema#/$defs/CancelRequestNotification',
}),
}

export function safeStream(stream: Stream, fail: () => void): Stream {
const reader = stream.readable.getReader()
const writer = stream.writable.getWriter()
const outstanding = new Set<string | number>()
let readerReleased = false
const releaseReader = () => {
if (!readerReleased) {
readerReleased = true
reader.releaseLock()
}
}
return {
writable: new WritableStream<AnyMessage>({
async write(message) {
if ('method' in message && 'id' in message && message.id !== null)
outstanding.add(message.id)
await writer.write(message)
},
async close() {
try {
await writer.close()
} finally {
writer.releaseLock()
}
},
async abort() {
try {
await writer.abort(new Error('ACP connection closed'))
} finally {
writer.releaseLock()
}
},
}),
readable: new ReadableStream<AnyMessage>({
async pull(controller) {
try {
const item = await reader.read()
if (item.done) {
releaseReader()
controller.close()
return
}
const message = item.value
if (!messageValid(message)) throw new Error('Invalid ACP envelope')
if ('method' in message) {
const check = parameterValidators[message.method as keyof typeof parameterValidators]
if (!('id' in message) && !check) throw new Error('Unsupported ACP notification')
if (check && !check(message.params)) throw new Error('Invalid ACP parameters')
} else if ('id' in message) {
if (message.id === null || !outstanding.delete(message.id))
throw new Error('Uncorrelated ACP response')
}
controller.enqueue(message)
} catch {
fail()
controller.error(new Error('ACP transport failed'))
await reader.cancel().catch(() => undefined)
releaseReader()
}
},
async cancel() {
if (readerReleased) return
try {
await reader.cancel()
} finally {
releaseReader()
}
},
}),
}
}
Loading
Loading