diff --git a/CHANGELOG.md b/CHANGELOG.md index 70903b9..48fa339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,21 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The Har ## [Unreleased] — v2 (in progress) -Work on the **v2 milestone** has begun. v2 is **additive**: it introduces two new protocol layers on top of the stable Schema layer and does **not** change `harness.yaml` — the `version` field stays `"1"` and the `harness.schema.json` `$id` (`/schema/v1/...`) is frozen. A fragment authored against v1 is Exchange-compatible without modification. New v2 schema artifacts are grouped under the `/schema/v2/` `$id` namespace. +Work on the **v2 milestone** continues. v2 is **additive**: it introduces two new protocol layers on top of the stable Schema layer and does **not** change `harness.yaml` — the `version` field stays `"1"` and the `harness.schema.json` `$id` (`/schema/v1/...`) is frozen. A fragment authored against v1 is Exchange-compatible without modification. New v2 schema artifacts are grouped under the `/schema/v2/` `$id` namespace. ### Added (draft) -- **Exchange layer:** the signed **offer envelope** (`exchange.schema.json`) and the consent-first `Offer → Preview → Accept / Edit / Reject → Apply` flow for peer-to-peer (1:1) fragment sharing — "AirDrop for harnesses." ed25519 sender identity; optional X25519 payload encryption. Normative draft in [protocol/exchange.md](protocol/exchange.md), specified by [HEP-7](heps/hep-0007-exchange-layer.md). *Status: Review.* -- **Registry layer:** hosted discovery at harnessprotocol.io — indexing of public `owner/repo` profiles/fragments/plugins, search, SHA-256 integrity hashing, and an append-only transparency log (`registry.schema.json`). GitHub stays authoritative; the registry is a discovery convenience, not a trust anchor. Normative draft in [protocol/registry.md](protocol/registry.md), specified by [HEP-8](heps/hep-0008-registry-layer.md). *Status: Review.* Verified authors, curation, and minisign registry signing are deferred to v3. +- **Registry layer:** hosted discovery at harnessprotocol.io — indexing of public `owner/repo` profiles/fragments/plugins, search, SHA-256 integrity hashing, and an append-only transparency log (`registry.schema.json`). GitHub stays authoritative; the registry is a discovery convenience, not a trust anchor. Normative draft in [protocol/registry.md](protocol/registry.md), specified by [HEP-8](heps/hep-0008-registry-layer.md). *Status: Review.* Verified authors, curation, and minisign registry signing are deferred to v3. The service prototype (hosted index, registration/discovery APIs, transparency-log server) required for Accepted has not started. -These layers are in **Review** status under the HEP process and are not yet released. Schema mirrors under `website/public/schema/v2/` are published at release, not during draft. +This layer is in **Review** status under the HEP process and is not yet released. Its schema mirror under `website/public/schema/v2/` is published at release, not during draft. + +## [v1.1.0] — 2026-07-27 + +**Exchange layer accepted** — the first of the two v2-milestone layers to ship; Registry remains in Review (see Unreleased above). [HEP-7](heps/hep-0007-exchange-layer.md) moves from Review to **Accepted**: both the format prototype (schema, examples, eval tests) and the runtime prototype (ed25519/X25519 signing and verification, canonicalization, and the `harness exchange keygen/offer/accept` flow, shipped in [harness-kit](https://github.com/harnessprotocol/harness-kit)) are satisfied. Backward-compatible: Exchange adds no `harness.yaml` fields, the `version` field stays `"1"`, and the v1 schema `$id` is unchanged. + +### Added + +- **Exchange layer:** the signed **offer envelope** (`exchange.schema.json`) and the consent-first `Offer → Preview → Accept / Edit / Reject → Apply` flow for peer-to-peer (1:1) fragment sharing — "AirDrop for harnesses." ed25519 sender identity; optional X25519 payload encryption. Normative in [protocol/exchange.md](protocol/exchange.md), specified by [HEP-7](heps/hep-0007-exchange-layer.md). The `schema/draft/` schema is snapshotted to `schema/2026-07-27/` and published to `website/public/schema/v2/exchange.schema.json`. ## [v1.0.0] — 2026-06-05 diff --git a/README.md b/README.md index c13cf5a..0682979 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ The specification is organized into three layers, each building on the previous. | Layer | Description | Status | |-------|-------------|--------| | **Schema** | The `harness.yaml` format, JSON Schema validation, security model, plugin manifest | v1 — current | -| **Exchange** | Harness-to-harness sharing: publish, fetch, and compose harnesses across tools and teams | v2 — draft ([HEP-7](heps/hep-0007-exchange-layer.md)) | +| **Exchange** | Harness-to-harness sharing: publish, fetch, and compose harnesses across tools and teams | v2 — accepted ([HEP-7](heps/hep-0007-exchange-layer.md)) | | **Registry** | Hosted discovery at harnessprotocol.io: search, publish, version resolution, integrity verification | v2/v3 — draft ([HEP-8](heps/hep-0008-registry-layer.md)) | Layers are intentionally decoupled. A tool can implement Schema-layer validation today without any dependency on exchange or registry infrastructure. @@ -104,7 +104,7 @@ Full documentation is available at [harnessprotocol.io/spec](https://harnessprot | [protocol/inheritance.md](protocol/inheritance.md) | `extends` resolution order and per-section merge rules | | [protocol/application.md](protocol/application.md) | Application pipeline, effective configuration, error handling | | [protocol/source-resolution.md](protocol/source-resolution.md) | Source resolution algorithm for `owner/repo` and local path references | -| [protocol/exchange.md](protocol/exchange.md) | *(v2 draft)* Exchange layer — the signed offer envelope and consent-first sharing flow | +| [protocol/exchange.md](protocol/exchange.md) | *(v2)* Exchange layer — the signed offer envelope and consent-first sharing flow | | [protocol/registry.md](protocol/registry.md) | *(v2 draft)* Registry layer — hosted discovery, integrity hashing, and the transparency log | | [security/threat-model.md](security/threat-model.md) | Threat model and security design | | [security/trust-boundaries.md](security/trust-boundaries.md) | Trust boundaries between spec, implementations, profiles, and remote content | @@ -113,7 +113,7 @@ Full documentation is available at [harnessprotocol.io/spec](https://harnessprot | [security/integrity.md](security/integrity.md) | Content integrity verification | | [security/instruction-injection.md](security/instruction-injection.md) | Instruction injection threat and mitigations | | [security/skill-injection.md](security/skill-injection.md) | Skill behavioral injection threat and mitigations | -| [security/exchange.md](security/exchange.md) | *(v2 draft)* Exchange threat model — authenticity, consent, confidentiality in transit | +| [security/exchange.md](security/exchange.md) | *(v2)* Exchange threat model — authenticity, consent, confidentiality in transit | | [security/registry.md](security/registry.md) | *(v2 draft)* Registry threat model — the registry is an index, not a trust anchor | | [security/crypto-map.md](security/crypto-map.md) | How SHA-256 integrity, ed25519 (Exchange), and minisign signing compose | diff --git a/heps/hep-0007-exchange-layer.md b/heps/hep-0007-exchange-layer.md index 2921b6c..ff76218 100644 --- a/heps/hep-0007-exchange-layer.md +++ b/heps/hep-0007-exchange-layer.md @@ -2,7 +2,7 @@ title: Exchange layer — peer-to-peer harness fragment sharing hep: 7 type: Standards Track -status: Review +status: Accepted authors: [siracusa5 ] sponsor: siracusa5 created: 2026-06-05 @@ -109,3 +109,5 @@ Per the Standards Track prototype requirement, this HEP's **format prototype is - Eval coverage: `eval/src/tests/schema/exchange.test.ts` — valid/invalid envelopes, the `oneOf` exclusivity, and the two-step "envelope valid but wrapped fragment invalid" check. The **runtime prototype** — the `harness exchange offer/accept` subcommands, ed25519 signing/verification, X25519 encryption, the relay API, and the consent-first preview UI — is a behavior of the reference implementation and is required in [harness-kit](https://github.com/harnessprotocol/harness-kit) before this HEP moves to **Accepted**. As with HEP-6, the in-repo artifacts verify the *declaration surface*; the prose specifies the apply-time semantics a conformant implementation MUST enforce. + +**Runtime prototype: satisfied.** `harness-kit` ships `keygen`, `offer`, and `accept` CLI subcommands (`apps/cli/src/commands/exchange.ts`) backed by `@harness-kit/exchange` (`packages/exchange`): ed25519 keypair generation and fingerprinting, canonical-JSON signing/verification, X25519 payload encryption, and the consent-first Preview → Accept/Edit/Reject → Apply flow, with test coverage for the envelope and keypair logic. The one named piece not yet built is the **relay API**; per this HEP that is non-blocking, since clipboard/file transport already covers the zero-infrastructure MVP. On that basis this HEP moves to **Accepted**. diff --git a/protocol/exchange.md b/protocol/exchange.md index 72c327c..173d6d1 100644 --- a/protocol/exchange.md +++ b/protocol/exchange.md @@ -2,7 +2,7 @@ This document specifies the **Exchange layer** of the Harness Protocol: a push-based, consent-required flow for sharing a single `kind: fragment` document from one person to another. Exchange is introduced in the v2 milestone and is specified normatively by [HEP-7](../heps/hep-0007-exchange-layer.md). The offer envelope is validated by [`schema/draft/exchange.schema.json`](../schema/draft/exchange.schema.json). -> **Status:** Draft (v2). The format is specified here; runtime behavior is implemented in the reference implementation. Normative language ("MUST", "SHOULD", etc.) follows [BCP 14](https://www.rfc-editor.org/info/bcp14), as in the rest of the specification. +> **Status:** Accepted (v2). The format is specified here; runtime behavior is implemented in the reference implementation. Normative language ("MUST", "SHOULD", etc.) follows [BCP 14](https://www.rfc-editor.org/info/bcp14), as in the rest of the specification. --- diff --git a/protocol/overview.md b/protocol/overview.md index 625c72d..538660a 100644 --- a/protocol/overview.md +++ b/protocol/overview.md @@ -12,15 +12,15 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S ## Protocol Layers -The Harness Protocol is organized into three layers, each building on the previous. Version 1 delivers the Schema layer. The Exchange and Registry layers are in draft for v2 — see [HEP-7](../heps/hep-0007-exchange-layer.md) and [HEP-8](../heps/hep-0008-registry-layer.md), and the normative drafts in [Exchange](./exchange.md) and [Registry](./registry.md). +The Harness Protocol is organized into three layers, each building on the previous. Version 1 delivers the Schema layer. The Exchange layer is accepted for v2 ([HEP-7](../heps/hep-0007-exchange-layer.md), normative spec in [Exchange](./exchange.md)); the Registry layer remains in draft for v2 ([HEP-8](../heps/hep-0008-registry-layer.md), normative draft in [Registry](./registry.md)). | Layer | Description | Status | |-------|-------------|--------| | **Schema** | The `harness.yaml` format: structure, validation rules, security model, inheritance semantics | v1 (current) | -| **Exchange** | Harness-to-harness sharing — a protocol for publishing, fetching, and composing harnesses between tools and teams ("AirDrop for harnesses") | v2 (draft — HEP-7) | +| **Exchange** | Harness-to-harness sharing — a protocol for publishing, fetching, and composing harnesses between tools and teams ("AirDrop for harnesses") | v2 (accepted — HEP-7) | | **Registry** | Hosted discovery at harnessprotocol.io — search, publish, version resolution, integrity verification for the broader ecosystem | v2/v3 (draft — HEP-8) | -The layers are intentionally decoupled. A tool can implement Schema-layer validation today without any dependency on exchange or registry infrastructure. When Exchange ships, tools opt in incrementally. +The layers are intentionally decoupled. A tool can implement Schema-layer validation today without any dependency on exchange or registry infrastructure. Exchange has shipped; tools opt in incrementally. Registry will follow the same pattern once accepted. ## What v1 Delivers @@ -90,7 +90,7 @@ MCP, AGENTS.md, and Agent Skills are stewarded under the **Agentic AI Foundation | [Environment](./environment.md) | Environment variable declarations, sensitive handling | | [Fragments](./fragments.md) | `kind: fragment` — partial harness documents for composition | | [Source Resolution](./source-resolution.md) | Source resolution algorithm for `owner/repo` and local path references | -| [Exchange](./exchange.md) | *(v2 draft)* The signed offer envelope and consent-first peer-to-peer sharing flow | +| [Exchange](./exchange.md) | *(v2)* The signed offer envelope and consent-first peer-to-peer sharing flow | | [Registry](./registry.md) | *(v2 draft)* Hosted discovery, integrity hashing, namespace design, transparency log | | [Application](./application.md) | Application pipeline, effective configuration, error handling | | [Inheritance](./inheritance.md) | `extends` resolution order and per-section merge rules | diff --git a/schema/2026-07-27/exchange.schema.json b/schema/2026-07-27/exchange.schema.json new file mode 100644 index 0000000..a9c6607 --- /dev/null +++ b/schema/2026-07-27/exchange.schema.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://harnessprotocol.io/schema/v2/exchange.schema.json", + "title": "Harness Protocol v2 — Exchange Offer Envelope", + "description": "Schema for Exchange-layer offer envelopes: a signed wrapper that carries a harness fragment from a sender to a receiver through the consent-first exchange flow. The wrapped fragment is opaque to this schema; it MUST independently validate against the harness schema (https://harnessprotocol.io/schema/v1/harness.schema.json) with kind: fragment semantics.", + "type": "object", + "required": ["version", "type", "sender", "expires", "signature"], + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema URI for editor tooling. Recommended value: 'https://harnessprotocol.io/schema/v2/exchange.schema.json'." + }, + "version": { + "type": "string", + "const": "1", + "description": "Offer-envelope format version. Must be the string '1'. This is the Exchange layer's own format version — independent of the harness.yaml 'version' field and of the v2 milestone label. Receivers MUST reject envelopes with an unrecognized version and surface a clear error." + }, + "type": { + "type": "string", + "enum": ["offer"], + "description": "Envelope type. Only 'offer' is defined in this version. Receivers MUST reject unknown types. The value 'receipt' is reserved for a future version (signed acknowledgement of receipt)." + }, + "sender": { + "type": "object", + "required": ["key"], + "description": "The sender's self-sovereign identity. The only authenticated identity is the key; 'display' is advisory.", + "properties": { + "key": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "The sender's ed25519 public key, lowercase hex-encoded (32 bytes / 64 hex chars). This key is the sender's identity. Receivers SHOULD show a fingerprint derived from it (see the Exchange specification) rather than the raw key." + }, + "display": { + "type": "string", + "maxLength": 64, + "description": "Optional, UNVERIFIED display name. A hint only — receivers MUST NOT treat it as authenticated identity." + } + }, + "additionalProperties": false + }, + "recipient": { + "type": "object", + "required": ["key"], + "description": "The intended recipient. REQUIRED for encrypted envelopes (the payload is encrypted to this key) and absent for unaddressed plaintext offers.", + "properties": { + "key": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "The recipient's ed25519 public key, lowercase hex-encoded. Converted to its X25519 equivalent to derive the shared secret for payload encryption." + } + }, + "additionalProperties": false + }, + "message": { + "type": "string", + "maxLength": 1024, + "description": "Optional human-readable message from the sender, displayed to the receiver at preview time. Not covered by the signature (it is not security-critical content)." + }, + "fragment": { + "type": "object", + "description": "The plaintext fragment being offered: a harness document with kind: fragment, serialized as a JSON object (YAML-equivalent fields, JSON types). Opaque to this schema; it MUST independently validate against the harness schema with fragment semantics before the envelope is considered well-formed. Present only in unencrypted envelopes; mutually exclusive with 'encrypted-fragment'." + }, + "encrypted-fragment": { + "type": "object", + "required": ["algorithm", "nonce", "ciphertext"], + "description": "The fragment encrypted to the recipient's key. Present only in encrypted envelopes; mutually exclusive with 'fragment'. The receiver decrypts and then validates the decrypted content against the harness schema.", + "properties": { + "algorithm": { + "type": "string", + "const": "x25519-xsalsa20-poly1305", + "description": "Authenticated encryption scheme: X25519 key agreement with XSalsa20-Poly1305 (the NaCl 'box' construction)." + }, + "nonce": { + "type": "string", + "contentEncoding": "base64", + "description": "Base64-encoded encryption nonce." + }, + "ciphertext": { + "type": "string", + "contentEncoding": "base64", + "description": "Base64-encoded ciphertext of the canonical-JSON fragment bytes." + } + }, + "additionalProperties": false + }, + "suggested-import-mode": { + "type": "string", + "enum": ["merge", "replace", "skip"], + "description": "A non-binding hint about how the fragment should be applied. The receiver sets the actual import-mode and MAY override this at the Accept step." + }, + "expires": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp after which the offer is stale. Receivers MUST NOT apply an expired offer. This bounds replay: an offer intercepted in transit cannot be applied indefinitely." + }, + "signature": { + "type": "string", + "pattern": "^[a-f0-9]{128}$", + "description": "Detached ed25519 signature, lowercase hex-encoded (64 bytes / 128 hex chars), produced by the sender's private key. For plaintext envelopes it covers the canonical-JSON bytes of 'fragment'; for encrypted envelopes it covers the raw 'encrypted-fragment.ciphertext' bytes. Receivers MUST verify the signature before preview; verification failure is a hard rejection with no 'proceed anyway' path." + } + }, + "oneOf": [ + { + "title": "Plaintext offer", + "required": ["fragment"], + "not": { "anyOf": [{ "required": ["encrypted-fragment"] }, { "required": ["recipient"] }] } + }, + { + "title": "Encrypted offer", + "required": ["encrypted-fragment", "recipient"], + "not": { "required": ["fragment"] } + } + ], + "patternProperties": { + "^x-": { + "description": "Implementation-specific extension field. The x- prefix signals a non-standard field. Implementations MUST ignore unrecognized x- fields to ensure forward compatibility." + } + }, + "additionalProperties": false +} diff --git a/schema/2026-07-27/harness.schema.json b/schema/2026-07-27/harness.schema.json new file mode 100644 index 0000000..aa4e826 --- /dev/null +++ b/schema/2026-07-27/harness.schema.json @@ -0,0 +1,715 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://harnessprotocol.io/schema/v1/harness.schema.json", + "title": "Harness Protocol v1", + "description": "Schema for harness.yaml profiles following the Harness Protocol v1 specification. Validates both full profiles and partial fragments.", + "type": "object", + "required": ["version"], + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema URI for editor tooling. Recommended value: 'https://harnessprotocol.io/schema/v1/harness.schema.json'." + }, + "version": { + "type": "string", + "const": "1", + "description": "Harness Protocol format version. Must be the string '1' (not the integer 1). The string form distinguishes this document as Harness Protocol format rather than the legacy harness-kit format." + }, + "kind": { + "type": "string", + "enum": ["profile", "fragment"], + "default": "profile", + "description": "Document type. 'profile' is a complete, self-contained harness configuration. 'fragment' is a partial piece intended for sharing or composition. Fragments skip required-field validation that applies to full profiles." + }, + "metadata": { + "type": "object", + "required": ["name", "description"], + "description": "Profile identity and discovery metadata. Required for profiles; optional for fragments.", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$", + "description": "Lowercase kebab-case profile identifier. Max 64 characters. Characters: [a-z0-9-]. Must start and end with alphanumeric." + }, + "description": { + "type": "string", + "maxLength": 256, + "description": "Human-readable description of this harness profile. Max 256 characters." + }, + "author": { + "type": "object", + "required": ["name"], + "description": "Profile author information.", + "properties": { + "name": { + "type": "string", + "description": "Author display name." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Author URL (personal homepage, GitHub profile, etc.)." + } + }, + "additionalProperties": false + }, + "version": { + "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": "Semantic version of this profile (not the protocol version). Follows semver 2.0.0. Used for dependency resolution when this profile is extended by others." + }, + "license": { + "type": "string", + "description": "SPDX license expression. Examples: 'Apache-2.0', 'MIT', 'CC-BY-4.0'." + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 32 + }, + "maxItems": 10, + "uniqueItems": true, + "description": "Discovery and classification tags. Max 10 tags, each max 32 characters." + } + }, + "additionalProperties": false + }, + "plugins": { + "type": "array", + "description": "Plugins to install into the harness. Plugins extend harness capabilities with skills, agents, and configuration.", + "items": { + "type": "object", + "required": ["name", "source"], + "description": "A plugin declaration.", + "properties": { + "name": { + "type": "string", + "description": "Plugin identifier. Must match the plugin's declared name in its manifest." + }, + "source": { + "type": "string", + "pattern": "^[a-zA-Z0-9_][a-zA-Z0-9_.-]*/[a-zA-Z0-9_][a-zA-Z0-9_.-]*$", + "description": "Source repository in 'owner/repo' format. Example: 'harnessprotocol/harness-kit'. Used to locate and fetch the plugin. Replaces the legacy 'marketplace' indirection." + }, + "version": { + "type": "string", + "description": "Semver range constraint. Examples: '>=0.2.0', '^1.0.0', '1.2.3'. If absent, the implementation selects the latest compatible version." + }, + "description": { + "type": "string", + "description": "Human-readable description for this plugin in this profile's context. Overrides the plugin's own description for display purposes." + }, + "config": { + "type": "object", + "description": "Plugin-specific configuration. The shape of this object is defined by the plugin manifest.", + "additionalProperties": true + }, + "loading": { + "type": "string", + "enum": ["eager", "deferred"], + "default": "eager", + "description": "Controls when the plugin's tools and context are loaded into the agent's context window. 'eager' loads at session start (default). 'deferred' loads on first invocation, reducing initial context size." + }, + "integrity": { + "type": "object", + "description": "Content integrity verification. Optional in v1; required in v2. Implementations SHOULD verify the hash when present and WARN when absent.", + "properties": { + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "Lowercase hex-encoded SHA-256 hash of the plugin archive." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "skills": { + "type": "array", + "description": "Skills to make available in the harness. A skill is a portable, named capability — a directory containing a SKILL.md file — that an agent loads on demand. Skills may be bundled by a plugin or declared directly here; this section declares them directly without requiring a full plugin.", + "items": { + "type": "object", + "required": ["name"], + "description": "A skill declaration. 'source' is required unless the entry only suppresses an inherited skill via 'enabled: false'.", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$", + "description": "Skill identifier in lowercase kebab-case. Max 64 characters. Should match the skill's SKILL.md frontmatter name." + }, + "source": { + "type": "string", + "description": "Where the skill is fetched from: 'owner/repo', 'owner/repo/path/to/skill' (GitHub), or './local/path' (local path). Resolves per the Source Resolution algorithm." + }, + "version": { + "type": "string", + "description": "Semver range constraint for owner/repo sources. Examples: '>=1.0.0', '^2.1.0', '1.2.3'. Ignored for local-path sources." + }, + "description": { + "type": "string", + "description": "Human-readable description for this skill in this profile's context. Overrides the skill's own description for display purposes." + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether this skill is active. Set to false to declare a skill without activating it — for example, to disable a skill inherited from a parent profile." + }, + "loading": { + "type": "string", + "enum": ["eager", "deferred"], + "default": "deferred", + "description": "When the skill's full content is loaded. 'deferred' (default) loads only the skill's metadata at session start and the body on first invocation. 'eager' loads the full skill at session start." + }, + "integrity": { + "type": "object", + "description": "Content integrity verification. Implementations SHOULD verify the hash when present and WARN when absent for skills fetched from external sources.", + "properties": { + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "Lowercase hex-encoded SHA-256 hash of the skill archive." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "if": { + "properties": { "enabled": { "const": false } }, + "required": ["enabled"] + }, + "then": {}, + "else": { "required": ["source"] } + } + }, + "architectural-constraints": { + "type": "object", + "description": "Declarative constraints enforcing architectural patterns, module boundaries, and structural invariants. Three enforcement levels: deterministic (linters, structural tests — cannot be overridden), review (LLM-based, can request exceptions), advisory (warnings, may be silenced).", + "properties": { + "linters": { + "type": "array", + "description": "Deterministic enforcement rules: naming conventions, module boundaries, code patterns. Violations block commits or merges.", + "items": { + "type": "object", + "required": ["name", "description"], + "properties": { + "name": { + "type": "string", + "description": "Linter identifier (e.g., 'module-boundary-checker', 'naming-convention')." + }, + "description": { + "type": "string", + "description": "What invariant this linter enforces." + }, + "enforcement": { + "type": "string", + "enum": ["block", "warn"], + "default": "block", + "description": "'block' = violations prevent merge. 'warn' = violations logged but don't prevent merge." + }, + "config": { + "type": "object", + "description": "Tool-specific linter configuration (keys are tool-dependent; e.g., eslint-compatible rules, ArchUnit assertions).", + "additionalProperties": true + }, + "source": { + "type": "string", + "description": "Where this linter is defined: 'custom' (in this harness), or a GitHub path (e.g., 'owner/repo/path/to/linter.md')." + } + }, + "additionalProperties": false + } + }, + "structural-tests": { + "type": "array", + "description": "Programmatic tests verifying architectural invariants (e.g., ArchUnit, layered architecture tests, module isolation). Failures block deployment.", + "items": { + "type": "object", + "required": ["name", "description"], + "properties": { + "name": { + "type": "string", + "description": "Test identifier (e.g., 'module-isolation', 'layered-architecture')." + }, + "description": { + "type": "string", + "description": "What architectural invariant this test verifies." + }, + "entrypoint": { + "type": "string", + "description": "Command to run the test (e.g., 'gradle architectureTest', 'python -m pytest tests/architecture/')." + }, + "enforcement": { + "type": "string", + "enum": ["block", "warn"], + "default": "block", + "description": "'block' = test failures prevent merge. 'warn' = failures logged but don't prevent merge." + }, + "source": { + "type": "string", + "description": "Where this test is defined: 'custom' (in this harness), or a GitHub path." + } + }, + "additionalProperties": false + } + }, + "review-policy": { + "type": "object", + "description": "LLM-based review policies: what patterns agents should watch for, when to flag for human review.", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether LLM review is active for this harness." + }, + "model": { + "type": "string", + "description": "Model to use for review (implementation-specific; e.g., 'gpt-4', 'claude-opus')." + }, + "patterns": { + "type": "array", + "description": "Architectural patterns to verify. Each pattern is a prose guideline for the review agent.", + "items": { + "type": "object", + "required": ["name", "rule"], + "properties": { + "name": { + "type": "string", + "description": "Pattern name (e.g., 'module-cohesion', 'circular-dependency-prevention')." + }, + "rule": { + "type": "string", + "description": "Prose description of what the pattern enforces and why." + }, + "severity": { + "type": "string", + "enum": ["error", "warning", "info"], + "default": "warning", + "description": "'error' = blocks merge if reviewer detects violation. 'warning' = flagged but merge allowed. 'info' = noted but non-blocking." + } + }, + "additionalProperties": false + } + }, + "guidance": { + "type": "string", + "description": "Optional prose guidance document describing the harness's architectural philosophy. Agents use this to calibrate reviews." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "mcp-servers": { + "type": "object", + "description": "MCP (Model Context Protocol) server declarations. Keys are server names used to reference them in the harness. Values describe how to connect to the server.", + "additionalProperties": { + "description": "An MCP server declaration. The 'transport' field discriminates between local (stdio) and remote (streamable-http/sse/ws) servers.", + "oneOf": [ + { + "title": "stdio transport", + "description": "MCP server communicating via standard I/O. The harness launches this as a local process.", + "type": "object", + "required": ["transport", "command"], + "properties": { + "transport": { + "type": "string", + "const": "stdio", + "description": "Transport type. 'stdio' starts a local subprocess and communicates over stdin/stdout." + }, + "command": { + "type": "string", + "description": "Executable to launch. May be a PATH-resolved binary (e.g., 'uvx', 'npx') or an absolute path." + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Command-line arguments. Values may reference env[] declarations as ${VAR_NAME}." + }, + "env": { + "type": "object", + "description": "Additional environment variables for the server process. Values may reference env[] declarations as ${VAR_NAME}. Every ${VAR} reference must have a corresponding env[] entry.", + "additionalProperties": { + "type": "string" + } + }, + "source": { + "type": "string", + "description": "Optional provenance identifier for the server package: a registry identity in reverse-DNS form (e.g., 'io.github.owner/server') or 'owner/repo'. Declares where the server originates, for auditability. Does not change how 'command' is invoked." + }, + "version": { + "type": "string", + "description": "Optional version or semver range for the server package, complementing any version pinned in 'args'." + }, + "integrity": { + "type": "object", + "description": "Optional content integrity verification for the server package, where verifiable. Implementations SHOULD verify the hash when present and WARN when absent for servers fetched from external sources.", + "properties": { + "sha256": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "Lowercase hex-encoded SHA-256 hash of the server package archive." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "title": "Remote transport (streamable-http / sse / ws)", + "description": "MCP server communicating over a network transport. Treated as untrusted remote content.", + "type": "object", + "required": ["transport", "url"], + "properties": { + "transport": { + "type": "string", + "enum": ["streamable-http", "http", "sse", "ws"], + "description": "Transport type. 'streamable-http' is the canonical remote transport; 'http' is an accepted alias for it. 'sse' (Server-Sent Events) is the legacy transport, retained for compatibility and deprecated for new servers. 'ws' (WebSocket) is non-standard and implementation-specific." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Server endpoint URL. Implementations MUST treat the server as untrusted remote content." + }, + "headers": { + "type": "object", + "description": "HTTP headers to include in requests. Values may reference env[] declarations as ${VAR_NAME}.", + "additionalProperties": { + "type": "string" + } + }, + "source": { + "type": "string", + "description": "Optional provenance identifier for the server: a registry identity in reverse-DNS form (e.g., 'io.github.owner/server') or 'owner/repo'. Declares where the server originates, for auditability." + }, + "version": { + "type": "string", + "description": "Optional version or semver range identifying the remote server build." + } + }, + "additionalProperties": false + } + ] + } + }, + "env": { + "type": "array", + "description": "Environment variable declarations. Declares every variable the harness needs — for documentation, user prompting, and security validation.", + "items": { + "type": "object", + "required": ["name", "description"], + "description": "An environment variable declaration.", + "properties": { + "name": { + "type": "string", + "pattern": "^[A-Z_][A-Z0-9_]*$", + "description": "Variable name in SCREAMING_SNAKE_CASE. Every ${VAR} reference in mcp-servers must have a matching entry here." + }, + "description": { + "type": "string", + "description": "REQUIRED. Human-readable explanation of what this variable is for. Shown to users when they need to provide it." + }, + "required": { + "type": "boolean", + "default": false, + "description": "Whether the harness will fail if this variable is absent. Default: false (optional)." + }, + "sensitive": { + "type": "boolean", + "default": true, + "description": "Whether this variable contains sensitive data (API keys, credentials, tokens). Default: true. When true, 'default' is FORBIDDEN — implementations MUST reject sensitive variables with defaults." + }, + "when": { + "type": "string", + "description": "Human-readable description of when this variable is needed. Displayed to users when prompting. Implementations MAY evaluate it as a condition expression (e.g., 'plugins contains data-lineage') to suppress prompting when false; when not evaluated, it is shown as informational text. Example: 'When accessing private GitHub repositories'." + }, + "default": { + "type": "string", + "description": "Default value for non-sensitive variables only. FORBIDDEN when sensitive is true (which is the default). Only valid when sensitive: false is explicitly set." + } + }, + "if": { + "required": ["sensitive"], + "properties": { + "sensitive": { + "const": false + } + } + }, + "then": {}, + "else": { + "not": { + "required": ["default"] + } + }, + "additionalProperties": false + } + }, + "instructions": { + "type": "object", + "description": "Instruction content to inject into the AI harness. Maps to harness-specific instruction files. All imported instructions receive provenance markers and are subordinate to the user's core safety rules.", + "properties": { + "operational": { + "description": "Operational instructions. Maps to CLAUDE.md (Claude Code) or equivalent. Inline text, a file:// path to a local file, or an https:// URL to a remote file.", + "oneOf": [ + { + "type": "string", + "description": "Inline text, 'file://relative/path', or 'https://...' URL." + }, + { + "type": "null", + "description": "Explicitly skip this instruction slot." + } + ] + }, + "behavioral": { + "description": "Behavioral preferences. Maps to AGENT.md (Claude Code) or equivalent. Inline text, file:// path, or https:// URL.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "identity": { + "description": "Identity context. Maps to SOUL.md (Claude Code). Set to null to skip. Not all harnesses support this slot — implementations that lack an identity slot SHOULD treat this as 'operational'.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "import-mode": { + "type": "string", + "enum": ["merge", "replace", "skip"], + "default": "merge", + "description": "How instructions are applied. 'merge' (default) appends to existing instructions, preserving user's safety rules. 'replace' overwrites — requires explicit user confirmation at apply time. 'skip' ignores all instructions in this profile." + } + }, + "additionalProperties": false + }, + "permissions": { + "type": "object", + "description": "Declarative capability intent. Self-documents what access this harness requires. Implementations enforce their own permission model — this is defense-in-depth documentation, not the enforcement boundary.", + "properties": { + "tools": { + "type": "object", + "description": "Tool access declarations.", + "properties": { + "allow": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tools this harness needs. Exact names or glob patterns. Examples: 'Read', 'Bash', 'mcp__*', 'mcp__github__*'." + }, + "deny": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tools this harness explicitly does not use. Glob patterns supported. Deny overrides allow when both match." + }, + "ask": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tools that should prompt for user confirmation before each use." + } + }, + "additionalProperties": false + }, + "paths": { + "type": "object", + "description": "Filesystem path access declarations.", + "properties": { + "writable": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Paths this harness may write to. Glob patterns supported. Example: 'src/', 'tests/'." + }, + "readonly": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Paths this harness reads but should not modify. Example: 'config/', '.env.example'." + } + }, + "additionalProperties": false + }, + "network": { + "type": "object", + "description": "Network access declarations.", + "properties": { + "allowed-hosts": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Hostnames or glob patterns this harness may contact. Examples: '*.github.com', 'api.openai.com'." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "policy": { + "type": "object", + "description": "Organization or team governance constraints. A policy acts as a ceiling: it constrains what extending or consuming profiles may grant and cannot be widened by them. A document with no 'policy' section imposes no managed constraints (the current behavior). The protocol declares intent; enforcement is the implementation's responsibility. See protocol/inheritance.md for precedence semantics.", + "properties": { + "mcp-servers": { + "type": "object", + "description": "Constraints on which MCP servers may be declared.", + "properties": { + "allowed-sources": { + "type": "array", + "items": { "type": "string" }, + "description": "Allowlist of permitted MCP server source identities or host patterns. When present, a declared server whose source/host matches none of these is rejected." + }, + "denied-sources": { + "type": "array", + "items": { "type": "string" }, + "description": "Denylist of forbidden MCP server source identities or host patterns. Deny overrides allow." + } + }, + "additionalProperties": false + }, + "plugins": { + "type": "object", + "description": "Constraints on which plugins may be installed.", + "properties": { + "allowed-sources": { + "type": "array", + "items": { "type": "string" }, + "description": "Allowlist of permitted plugin 'owner/repo' sources or patterns." + }, + "denied-sources": { + "type": "array", + "items": { "type": "string" }, + "description": "Denylist of forbidden plugin sources or patterns. Deny overrides allow." + }, + "allowed-marketplaces": { + "type": "array", + "items": { "type": "string" }, + "description": "Allowlist of marketplaces or registries from which plugins may be fetched." + } + }, + "additionalProperties": false + }, + "skills": { + "type": "object", + "description": "Constraints on which skills may be declared.", + "properties": { + "allowed-sources": { + "type": "array", + "items": { "type": "string" }, + "description": "Allowlist of permitted skill sources or patterns." + }, + "denied-sources": { + "type": "array", + "items": { "type": "string" }, + "description": "Denylist of forbidden skill sources or patterns. Deny overrides allow." + } + }, + "additionalProperties": false + }, + "permissions": { + "type": "object", + "description": "A ceiling on permission grants. Extending or consuming profiles MAY narrow these grants but MUST NOT widen them.", + "properties": { + "tools": { + "type": "object", + "properties": { + "allow": { + "type": "array", + "items": { "type": "string" }, + "description": "Maximum set of tools that may be granted. Glob patterns supported. A profile cannot grant a tool that matches none of these patterns." + }, + "deny": { + "type": "array", + "items": { "type": "string" }, + "description": "Tools that are always denied regardless of profile grants. Deny overrides allow." + } + }, + "additionalProperties": false + }, + "network": { + "type": "object", + "properties": { + "allowed-hosts": { + "type": "array", + "items": { "type": "string" }, + "description": "Maximum set of network hosts that may be contacted. A profile cannot allow a host that matches none of these patterns." + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "require-integrity": { + "type": "boolean", + "default": false, + "description": "When true, integrity verification is mandatory for all plugins, skills, and MCP server packages. Declarations without a verifiable integrity hash are rejected." + } + }, + "additionalProperties": false + }, + "extends": { + "type": "array", + "description": "Parent profiles to inherit from. The child's values override parents. Multiple parents merge left-to-right. See protocol/inheritance.md for full semantics.", + "items": { + "type": "object", + "required": ["source"], + "description": "A parent profile reference.", + "properties": { + "source": { + "type": "string", + "description": "Parent profile location. Format: 'owner/repo' (root harness.yaml) or 'owner/repo/path/to/harness.yaml'." + }, + "version": { + "type": "string", + "description": "Semver range constraint for the parent profile version. Example: '>=1.0.0'." + } + }, + "additionalProperties": false + } + } + }, + "if": { + "not": { + "required": ["kind"], + "properties": { + "kind": { + "const": "fragment" + } + } + } + }, + "then": { + "required": ["version", "metadata"] + }, + "patternProperties": { + "^x-": { + "description": "Implementation-specific extension field. The x- prefix signals a non-standard field. Implementations MUST ignore unrecognized x- fields to ensure forward compatibility." + } + }, + "additionalProperties": false +} diff --git a/schema/2026-07-27/plugin.schema.json b/schema/2026-07-27/plugin.schema.json new file mode 100644 index 0000000..28db4b4 --- /dev/null +++ b/schema/2026-07-27/plugin.schema.json @@ -0,0 +1,193 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://harnessprotocol.io/schema/v1/plugin.schema.json", + "title": "Harness Protocol v1 — Plugin Manifest", + "description": "Schema for plugin.json manifests. Plugin authors use this to declare their plugin's identity, capabilities, and requirements.", + "type": "object", + "required": ["name", "description", "version"], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$", + "description": "Plugin identifier in lowercase kebab-case. Must match the directory name used when the plugin is installed. Max 64 characters." + }, + "description": { + "type": "string", + "maxLength": 256, + "description": "One-sentence description of what this plugin does. Used in harness.yaml listings and discovery." + }, + "version": { + "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": "Plugin version following semver 2.0.0. Used for version constraint resolution when harness.yaml specifies a 'version' range." + }, + "author": { + "type": "object", + "required": ["name"], + "description": "Plugin author information.", + "properties": { + "name": { + "type": "string", + "description": "Author display name." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Author URL." + } + }, + "additionalProperties": false + }, + "license": { + "type": "string", + "description": "SPDX license expression. Examples: 'Apache-2.0', 'MIT'. Recommended for public plugins." + }, + "category": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]{0,30}[a-z0-9])?$", + "maxLength": 32, + "description": "Plugin category for discovery and marketplace organization. Lowercase kebab-case. Max 32 characters." + }, + "tags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 32 + }, + "maxItems": 10, + "uniqueItems": true, + "description": "Discovery and classification tags. Max 10 tags, each max 32 characters. Used for search and filtering." + }, + "skills": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of skills this plugin provides. Each skill corresponds to a SKILL.md file. Used for documentation and discovery — not for validation." + }, + "agents": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Names of sub-agents this plugin provides. Each agent corresponds to an agent definition file. Used for documentation and discovery." + }, + "requires": { + "type": "object", + "description": "Plugin requirements that must be satisfied for the plugin to function.", + "properties": { + "env": { + "type": "array", + "description": "Environment variables this plugin needs. Uses the same env item schema as harness.yaml env[]. Implementations SHOULD surface these to users during install.", + "items": { + "type": "object", + "required": ["name", "description"], + "description": "An environment variable requirement.", + "properties": { + "name": { + "type": "string", + "pattern": "^[A-Z_][A-Z0-9_]*$", + "description": "Variable name in SCREAMING_SNAKE_CASE." + }, + "description": { + "type": "string", + "description": "REQUIRED. Human-readable explanation of what this variable is for." + }, + "required": { + "type": "boolean", + "default": false, + "description": "Whether the plugin will fail without this variable." + }, + "sensitive": { + "type": "boolean", + "default": true, + "description": "Whether this variable contains sensitive data. Default: true. When true, 'default' is FORBIDDEN." + }, + "when": { + "type": "string", + "description": "Human-readable description of when this variable is needed. Displayed to users when prompting. Implementations MAY evaluate it as a condition expression to suppress prompting when false; when not evaluated, it is shown as informational text." + }, + "default": { + "type": "string", + "description": "Default value. FORBIDDEN when sensitive is true." + } + }, + "if": { + "required": ["sensitive"], + "properties": { + "sensitive": { + "const": false + } + } + }, + "then": {}, + "else": { + "properties": { + "default": false + } + }, + "additionalProperties": false + } + }, + "min-protocol": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)$", + "description": "Minimum Harness Protocol version required. Example: '1'. Implementations that encounter a plugin requiring a higher protocol version MUST warn the user." + } + }, + "additionalProperties": false + }, + "loading": { + "type": "string", + "enum": ["eager", "deferred"], + "default": "eager", + "description": "The plugin author's recommended loading mode. 'eager' loads all tools and context at session start (default). 'deferred' recommends loading on first invocation, reducing initial context size. Harness authors may override this in their plugins[] declaration." + }, + "config-schema": { + "type": "object", + "description": "JSON Schema for the plugin's config object (the 'config' field in harness.yaml plugins[]). When present, implementations SHOULD validate harness.yaml plugin config against this schema.", + "additionalProperties": true + }, + "mcp": { + "type": "object", + "description": "MCP server bundled with this plugin. The implementation starts this server alongside the plugin and registers its tools under a namespace derived from the plugin name.", + "required": ["server"], + "properties": { + "server": { + "type": "object", + "description": "Server declaration. Only stdio transport is supported for plugin-bundled servers.", + "required": ["transport", "command"], + "properties": { + "transport": { + "type": "string", + "const": "stdio", + "description": "Transport type. Plugin-bundled MCP servers use stdio (local subprocess)." + }, + "command": { + "type": "string", + "description": "Executable to launch. Supports ${CLAUDE_PLUGIN_ROOT} for plugin-relative paths." + }, + "args": { + "type": "array", + "items": { "type": "string" }, + "description": "Command-line arguments. Values may reference ${CLAUDE_PLUGIN_ROOT}." + }, + "env": { + "type": "object", + "description": "Additional environment variables for the server process.", + "additionalProperties": { "type": "string" } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "patternProperties": { + "^x-": { + "description": "Implementation-specific extension field. Implementations MUST ignore unrecognized x- fields." + } + }, + "additionalProperties": false +} diff --git a/website/content/docs/getting-started/index.mdx b/website/content/docs/getting-started/index.mdx index c247d30..a22441b 100644 --- a/website/content/docs/getting-started/index.mdx +++ b/website/content/docs/getting-started/index.mdx @@ -15,15 +15,15 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S ## Protocol Layers -The Harness Protocol is organized into three layers, each building on the previous. Version 1 delivers the Schema layer. The Exchange and Registry layers are in draft for v2 — see [Exchange](/docs/specification/exchange) and [Registry](/docs/specification/registry). +The Harness Protocol is organized into three layers, each building on the previous. Version 1 delivers the Schema layer. The Exchange layer is accepted for v2 — see [Exchange](/docs/specification/exchange). The Registry layer remains in draft for v2 — see [Registry](/docs/specification/registry). | Layer | Description | Status | |-------|-------------|--------| | **Schema** | The `harness.yaml` format: structure, validation rules, security model, inheritance semantics | v1 (current) | -| **Exchange** | Harness-to-harness sharing — a protocol for publishing, fetching, and composing harnesses between tools and teams ("AirDrop for harnesses") | v2 (draft — HEP-7) | +| **Exchange** | Harness-to-harness sharing — a protocol for publishing, fetching, and composing harnesses between tools and teams ("AirDrop for harnesses") | v2 (accepted — HEP-7) | | **Registry** | Hosted discovery at harnessprotocol.io — search, publish, version resolution, integrity verification for the broader ecosystem | v2/v3 (draft — HEP-8) | -The layers are intentionally decoupled. A tool can implement Schema-layer validation today without any dependency on exchange or registry infrastructure. When Exchange ships, tools opt in incrementally. +The layers are intentionally decoupled. A tool can implement Schema-layer validation today without any dependency on exchange or registry infrastructure. Exchange has shipped; tools opt in incrementally. Registry will follow the same pattern once accepted. ## What v1 Delivers diff --git a/website/content/docs/security/exchange.mdx b/website/content/docs/security/exchange.mdx index 3399c1c..51f5b0c 100644 --- a/website/content/docs/security/exchange.mdx +++ b/website/content/docs/security/exchange.mdx @@ -1,6 +1,6 @@ --- title: Exchange Security -description: "(v2 draft) Threat model for the Exchange layer — authenticity, consent, and confidentiality in transit." +description: "(v2) Threat model for the Exchange layer — authenticity, consent, and confidentiality in transit." --- This document specifies the threat model for the **Exchange layer** ([Exchange](/docs/specification/exchange), [HEP-7](https://github.com/harnessprotocol/harness-protocol/blob/main/heps/hep-0007-exchange-layer.md)). Exchange moves a fragment from a sender to a receiver over an untrusted channel, so its security properties are about **authenticity, consent, and confidentiality in transit** — not about making received content inherently trustworthy. An applied fragment is subject to the same trust constraints as any other `extends` entry; Exchange grants it no privilege. diff --git a/website/content/docs/security/index.mdx b/website/content/docs/security/index.mdx index 1c2941c..7b68585 100644 --- a/website/content/docs/security/index.mdx +++ b/website/content/docs/security/index.mdx @@ -15,7 +15,7 @@ The Security section defines the threat model and security properties that all c - + diff --git a/website/content/docs/specification/exchange.mdx b/website/content/docs/specification/exchange.mdx index 61625e6..aa3cfea 100644 --- a/website/content/docs/specification/exchange.mdx +++ b/website/content/docs/specification/exchange.mdx @@ -1,11 +1,11 @@ --- title: Exchange -description: "(v2 draft) The signed offer envelope and consent-first peer-to-peer fragment sharing flow." +description: "(v2) The signed offer envelope and consent-first peer-to-peer fragment sharing flow." --- This document specifies the **Exchange layer** of the Harness Protocol: a push-based, consent-required flow for sharing a single `kind: fragment` document from one person to another. Exchange is introduced in the v2 milestone and is specified normatively by [HEP-7](https://github.com/harnessprotocol/harness-protocol/blob/main/heps/hep-0007-exchange-layer.md). The offer envelope is validated by [`exchange.schema.json`](https://github.com/harnessprotocol/harness-protocol/blob/main/schema/draft/exchange.schema.json). -> **Status:** Draft (v2). The format is specified here; runtime behavior is implemented in the reference implementation. Normative language ("MUST", "SHOULD", etc.) follows [BCP 14](https://www.rfc-editor.org/info/bcp14), as in the rest of the specification. +> **Status:** Accepted (v2). The format is specified here; runtime behavior is implemented in the reference implementation. Normative language ("MUST", "SHOULD", etc.) follows [BCP 14](https://www.rfc-editor.org/info/bcp14), as in the rest of the specification. --- diff --git a/website/content/docs/specification/index.mdx b/website/content/docs/specification/index.mdx index fd64852..c5302b3 100644 --- a/website/content/docs/specification/index.mdx +++ b/website/content/docs/specification/index.mdx @@ -17,6 +17,6 @@ The Specification section contains the normative documents for the Harness Proto - + diff --git a/website/public/schema/v2/exchange.schema.json b/website/public/schema/v2/exchange.schema.json new file mode 100644 index 0000000..a9c6607 --- /dev/null +++ b/website/public/schema/v2/exchange.schema.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://harnessprotocol.io/schema/v2/exchange.schema.json", + "title": "Harness Protocol v2 — Exchange Offer Envelope", + "description": "Schema for Exchange-layer offer envelopes: a signed wrapper that carries a harness fragment from a sender to a receiver through the consent-first exchange flow. The wrapped fragment is opaque to this schema; it MUST independently validate against the harness schema (https://harnessprotocol.io/schema/v1/harness.schema.json) with kind: fragment semantics.", + "type": "object", + "required": ["version", "type", "sender", "expires", "signature"], + "properties": { + "$schema": { + "type": "string", + "description": "JSON Schema URI for editor tooling. Recommended value: 'https://harnessprotocol.io/schema/v2/exchange.schema.json'." + }, + "version": { + "type": "string", + "const": "1", + "description": "Offer-envelope format version. Must be the string '1'. This is the Exchange layer's own format version — independent of the harness.yaml 'version' field and of the v2 milestone label. Receivers MUST reject envelopes with an unrecognized version and surface a clear error." + }, + "type": { + "type": "string", + "enum": ["offer"], + "description": "Envelope type. Only 'offer' is defined in this version. Receivers MUST reject unknown types. The value 'receipt' is reserved for a future version (signed acknowledgement of receipt)." + }, + "sender": { + "type": "object", + "required": ["key"], + "description": "The sender's self-sovereign identity. The only authenticated identity is the key; 'display' is advisory.", + "properties": { + "key": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "The sender's ed25519 public key, lowercase hex-encoded (32 bytes / 64 hex chars). This key is the sender's identity. Receivers SHOULD show a fingerprint derived from it (see the Exchange specification) rather than the raw key." + }, + "display": { + "type": "string", + "maxLength": 64, + "description": "Optional, UNVERIFIED display name. A hint only — receivers MUST NOT treat it as authenticated identity." + } + }, + "additionalProperties": false + }, + "recipient": { + "type": "object", + "required": ["key"], + "description": "The intended recipient. REQUIRED for encrypted envelopes (the payload is encrypted to this key) and absent for unaddressed plaintext offers.", + "properties": { + "key": { + "type": "string", + "pattern": "^[a-f0-9]{64}$", + "description": "The recipient's ed25519 public key, lowercase hex-encoded. Converted to its X25519 equivalent to derive the shared secret for payload encryption." + } + }, + "additionalProperties": false + }, + "message": { + "type": "string", + "maxLength": 1024, + "description": "Optional human-readable message from the sender, displayed to the receiver at preview time. Not covered by the signature (it is not security-critical content)." + }, + "fragment": { + "type": "object", + "description": "The plaintext fragment being offered: a harness document with kind: fragment, serialized as a JSON object (YAML-equivalent fields, JSON types). Opaque to this schema; it MUST independently validate against the harness schema with fragment semantics before the envelope is considered well-formed. Present only in unencrypted envelopes; mutually exclusive with 'encrypted-fragment'." + }, + "encrypted-fragment": { + "type": "object", + "required": ["algorithm", "nonce", "ciphertext"], + "description": "The fragment encrypted to the recipient's key. Present only in encrypted envelopes; mutually exclusive with 'fragment'. The receiver decrypts and then validates the decrypted content against the harness schema.", + "properties": { + "algorithm": { + "type": "string", + "const": "x25519-xsalsa20-poly1305", + "description": "Authenticated encryption scheme: X25519 key agreement with XSalsa20-Poly1305 (the NaCl 'box' construction)." + }, + "nonce": { + "type": "string", + "contentEncoding": "base64", + "description": "Base64-encoded encryption nonce." + }, + "ciphertext": { + "type": "string", + "contentEncoding": "base64", + "description": "Base64-encoded ciphertext of the canonical-JSON fragment bytes." + } + }, + "additionalProperties": false + }, + "suggested-import-mode": { + "type": "string", + "enum": ["merge", "replace", "skip"], + "description": "A non-binding hint about how the fragment should be applied. The receiver sets the actual import-mode and MAY override this at the Accept step." + }, + "expires": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp after which the offer is stale. Receivers MUST NOT apply an expired offer. This bounds replay: an offer intercepted in transit cannot be applied indefinitely." + }, + "signature": { + "type": "string", + "pattern": "^[a-f0-9]{128}$", + "description": "Detached ed25519 signature, lowercase hex-encoded (64 bytes / 128 hex chars), produced by the sender's private key. For plaintext envelopes it covers the canonical-JSON bytes of 'fragment'; for encrypted envelopes it covers the raw 'encrypted-fragment.ciphertext' bytes. Receivers MUST verify the signature before preview; verification failure is a hard rejection with no 'proceed anyway' path." + } + }, + "oneOf": [ + { + "title": "Plaintext offer", + "required": ["fragment"], + "not": { "anyOf": [{ "required": ["encrypted-fragment"] }, { "required": ["recipient"] }] } + }, + { + "title": "Encrypted offer", + "required": ["encrypted-fragment", "recipient"], + "not": { "required": ["fragment"] } + } + ], + "patternProperties": { + "^x-": { + "description": "Implementation-specific extension field. The x- prefix signals a non-standard field. Implementations MUST ignore unrecognized x- fields to ensure forward compatibility." + } + }, + "additionalProperties": false +}